diff --git a/app/__init__.py b/app/__init__.py index 2470d6a..34e6215 100755 --- a/app/__init__.py +++ b/app/__init__.py @@ -10,9 +10,7 @@ logger = logging.getLogger(__name__) logger.setLevel(logging.DEBUG if config.DEBUG else logging.INFO) handler = logging.StreamHandler() formatter = uvicorn.logging.ColourizedFormatter( - "{levelprefix:<8} @{name}: {message}", - style="{", - use_colors=True + "{levelprefix:<8} @{name}: {message}", style="{", use_colors=True ) handler.setFormatter(formatter) logger.addHandler(handler) diff --git a/app/db/__init__.py b/app/db/__init__.py index 1f18c50..620f4eb 100644 --- a/app/db/__init__.py +++ b/app/db/__init__.py @@ -3,14 +3,32 @@ from sqlalchemy.orm import Session from .base import Base, SessionLocal, engine # noqa -from .crud import (create_admin, create_notification_reminder, # noqa - create_user, delete_notification_reminder, get_admin, - get_admins, get_jwt_secret_key, get_notification_reminder, - assure_node_inbounds, get_system_usage, - get_tls_certificate, get_user, get_user_by_id, get_users, - get_users_count, remove_admin, remove_user, revoke_user_sub, - set_owner, update_admin, update_user, update_user_status, - update_user_sub, start_user_expire) +from .crud import ( + create_admin, + create_notification_reminder, # noqa + create_user, + delete_notification_reminder, + get_admin, + get_admins, + get_jwt_secret_key, + get_notification_reminder, + assure_node_inbounds, + get_system_usage, + get_tls_certificate, + get_user, + get_user_by_id, + get_users, + get_users_count, + remove_admin, + remove_user, + revoke_user_sub, + set_owner, + update_admin, + update_user, + update_user_status, + update_user_sub, + start_user_expire, +) from .models import JWT, System, User # noqa @@ -50,17 +68,13 @@ __all__ = [ "update_admin", "remove_admin", "get_admins", - "create_notification_reminder", "get_notification_reminder", "delete_notification_reminder", - "GetDB", - "User", "System", "JWT", - "Base", "Session", ] diff --git a/app/db/base.py b/app/db/base.py index 0e3688d..fe6b922 100644 --- a/app/db/base.py +++ b/app/db/base.py @@ -3,13 +3,13 @@ from sqlalchemy import create_engine from sqlalchemy.orm import declarative_base from sqlalchemy.orm import sessionmaker -IS_SQLITE = SQLALCHEMY_DATABASE_URL.startswith('sqlite') +IS_SQLITE = SQLALCHEMY_DATABASE_URL.startswith("sqlite") if IS_SQLITE: engine = create_engine( SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False}, - #echo=True + # echo=True ) else: engine = create_engine( @@ -17,7 +17,7 @@ else: pool_size=10, max_overflow=30, pool_recycle=3600, - pool_timeout=10 + pool_timeout=10, ) SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) diff --git a/app/db/crud.py b/app/db/crud.py index aff13ef..19d7547 100644 --- a/app/db/crud.py +++ b/app/db/crud.py @@ -7,57 +7,87 @@ from typing import List, Optional, Tuple, Union from sqlalchemy import and_, delete, update, select from sqlalchemy.orm import Session -from app.db.models import (JWT, TLS, Admin, Node, NodeUsage, NodeUserUsage, - NotificationReminder, - InboundHost, Service, - Inbound, System, User, - UserUsageResetLogs) +from app.db.models import ( + JWT, + TLS, + Admin, + Node, + NodeUsage, + NodeUserUsage, + NotificationReminder, + InboundHost, + Service, + Inbound, + System, + User, + UserUsageResetLogs, +) from app.models.admin import AdminCreate, AdminModify, AdminPartialModify -from app.models.node import (NodeCreate, NodeModify, NodeStatus, - NodeUsageResponse) +from app.models.node import NodeCreate, NodeModify, NodeStatus, NodeUsageResponse from app.models.proxy import InboundHost as InboundHostModify from app.models.service import Service as ServiceModify, ServiceCreate -from app.models.user import (ReminderType, UserCreate, - UserDataLimitResetStrategy, UserModify, - UserStatus, UserUsageResponse) -from app.utils.helpers import (calculate_expiration_days, - calculate_usage_percent) +from app.models.user import ( + ReminderType, + UserCreate, + UserDataLimitResetStrategy, + UserModify, + UserStatus, + UserUsageResponse, +) +from app.utils.helpers import calculate_expiration_days, calculate_usage_percent from config import NOTIFY_DAYS_LEFT, NOTIFY_REACHED_USAGE_PERCENT def add_default_hosts(db: Session, inbounds: List[Inbound]): - hosts = [InboundHost(remark="🚀 Marz ({USERNAME}) [{PROTOCOL} - {TRANSPORT}]", address="{SERVER_IP}", inbound=i) for - i in inbounds] + hosts = [ + InboundHost( + remark="🚀 Marz ({USERNAME}) [{PROTOCOL} - {TRANSPORT}]", + address="{SERVER_IP}", + inbound=i, + ) + for i in inbounds + ] db.add_all(hosts) db.commit() def assure_node_inbounds(db: Session, inbounds: List[Inbound], node_id: int): - current_tags = [i[0] for i in db.execute(select(Inbound.tag).filter(Inbound.node_id == node_id)).all()] + current_tags = [ + i[0] + for i in db.execute( + select(Inbound.tag).filter(Inbound.node_id == node_id) + ).all() + ] updated_tags = set(i.tag for i in list(inbounds)) inbound_additions, tag_deletions = list(), set() for tag in current_tags: if tag not in updated_tags: tag_deletions.add(tag) - db_inbounds = db.query(Inbound).where(and_(Inbound.node_id == node_id, Inbound.tag.in_(tag_deletions))) + db_inbounds = db.query(Inbound).where( + and_(Inbound.node_id == node_id, Inbound.tag.in_(tag_deletions)) + ) for i in db_inbounds: db.delete(i) for inb in inbounds: if inb.tag in current_tags: - stmt = update(Inbound).where( - and_(Inbound.node_id == node_id, - Inbound.tag == inb.tag) - ).values( - protocol=json.loads(inb.config)["protocol"], config=inb.config + stmt = ( + update(Inbound) + .where(and_(Inbound.node_id == node_id, Inbound.tag == inb.tag)) + .values(protocol=json.loads(inb.config)["protocol"], config=inb.config) ) db.execute(stmt) else: inbound_additions.append(inb) - new_inbounds = [Inbound(tag=inb.tag, - protocol=json.loads(inb.config)["protocol"], - config=inb.config, - node_id=node_id) for inb in inbound_additions] + new_inbounds = [ + Inbound( + tag=inb.tag, + protocol=json.loads(inb.config)["protocol"], + config=inb.config, + node_id=node_id, + ) + for inb in inbound_additions + ] db.add_all(new_inbounds) db.flush() for i in new_inbounds: @@ -66,17 +96,31 @@ def assure_node_inbounds(db: Session, inbounds: List[Inbound], node_id: int): db.commit() -def get_node_users(db: Session, node_id: Optional[int], statuses: Optional[List[UserStatus]] = None): - query = db.query(User.id, User.username, User.key, Inbound).distinct( - ).join(Inbound.services).join(Service.users).filter(Inbound.node_id == node_id) +def get_node_users( + db: Session, node_id: Optional[int], statuses: Optional[List[UserStatus]] = None +): + query = ( + db.query(User.id, User.username, User.key, Inbound) + .distinct() + .join(Inbound.services) + .join(Service.users) + .filter(Inbound.node_id == node_id) + ) if isinstance(statuses, list): query = query.filter(User.status.in_(statuses)) return query.all() def get_user_hosts(db: Session, user_id: int): - return db.query(InboundHost).distinct().join(User.services).join(Service.inbounds).join(Inbound.hosts).filter( - User.id == user_id).all() + return ( + db.query(InboundHost) + .distinct() + .join(User.services) + .join(Service.inbounds) + .join(Inbound.hosts) + .filter(User.id == user_id) + .all() + ) def get_inbound_hosts(db: Session, inbound_id: int) -> List[InboundHost]: @@ -106,7 +150,7 @@ def add_host(db: Session, inbound: Inbound, host: InboundHostModify): inbound=inbound, security=host.security, alpn=host.alpn, - fingerprint=host.fingerprint + fingerprint=host.fingerprint, ) inbound.hosts.append(host) db.commit() @@ -129,7 +173,9 @@ def update_host(db: Session, db_host: InboundHost, host: InboundHostModify): return db_host -def update_hosts(db: Session, inbound_tag: str, modified_hosts: List[InboundHostModify]): +def update_hosts( + db: Session, inbound_tag: str, modified_hosts: List[InboundHostModify] +): inbound = get_or_create_inbound(db, inbound_tag) inbound.hosts = [ InboundHost( @@ -144,8 +190,9 @@ def update_hosts(db: Session, inbound_tag: str, modified_hosts: List[InboundHost alpn=host.alpn, fingerprint=host.fingerprint, allowinsecure=host.allowinsecure, - is_disabled=host.is_disabled - ) for host in modified_hosts + is_disabled=host.is_disabled, + ) + for host in modified_hosts ] db.commit() db.refresh(inbound) @@ -160,29 +207,34 @@ def get_user_by_id(db: Session, user_id: int): return db.query(User).filter(User.id == user_id).first() -UsersSortingOptions = Enum('UsersSortingOptions', { - 'username': User.username.asc(), - 'used_traffic': User.used_traffic.asc(), - 'data_limit': User.data_limit.asc(), - 'expire': User.expire.asc(), - 'created_at': User.created_at.asc(), - '-username': User.username.desc(), - '-used_traffic': User.used_traffic.desc(), - '-data_limit': User.data_limit.desc(), - '-expire': User.expire.desc(), - '-created_at': User.created_at.desc(), -}) - - -def get_users(db: Session, - offset: Optional[int] = None, - limit: Optional[int] = None, - usernames: Optional[List[str]] = None, - status: Optional[Union[UserStatus, list]] = None, - sort: Optional[List[UsersSortingOptions]] = None, - admin: Optional[Admin] = None, - reset_strategy: Optional[Union[UserDataLimitResetStrategy, list]] = None, - return_with_count: bool = False) -> Union[List[User], Tuple[List[User], int]]: +UsersSortingOptions = Enum( + "UsersSortingOptions", + { + "username": User.username.asc(), + "used_traffic": User.used_traffic.asc(), + "data_limit": User.data_limit.asc(), + "expire": User.expire.asc(), + "created_at": User.created_at.asc(), + "-username": User.username.desc(), + "-used_traffic": User.used_traffic.desc(), + "-data_limit": User.data_limit.desc(), + "-expire": User.expire.desc(), + "-created_at": User.created_at.desc(), + }, +) + + +def get_users( + db: Session, + offset: Optional[int] = None, + limit: Optional[int] = None, + usernames: Optional[List[str]] = None, + status: Optional[Union[UserStatus, list]] = None, + sort: Optional[List[UsersSortingOptions]] = None, + admin: Optional[Admin] = None, + reset_strategy: Optional[Union[UserDataLimitResetStrategy, list]] = None, + return_with_count: bool = False, +) -> Union[List[User], Tuple[List[User], int]]: query = db.query(User) if usernames: @@ -224,20 +276,24 @@ def get_users(db: Session, return query.all() -def get_user_usages(db: Session, dbuser: User, start: datetime, end: datetime, - ) -> List[UserUsageResponse]: +def get_user_usages( + db: Session, + dbuser: User, + start: datetime, + end: datetime, +) -> List[UserUsageResponse]: usages = dict() for node in db.query(Node).all(): usages[node.id] = UserUsageResponse( - node_id=node.id, - node_name=node.name, - used_traffic=0 + node_id=node.id, node_name=node.name, used_traffic=0 ) - cond = and_(NodeUserUsage.user_id == dbuser.id, - NodeUserUsage.created_at >= start, - NodeUserUsage.created_at <= end) + cond = and_( + NodeUserUsage.user_id == dbuser.id, + NodeUserUsage.created_at >= start, + NodeUserUsage.created_at <= end, + ) for v in db.query(NodeUserUsage).filter(cond): try: @@ -262,7 +318,9 @@ def create_user(db: Session, user: UserCreate, admin: Admin = None): username=user.username, # proxies=proxies, key=user.key, - services=db.query(Service).filter(Service.id.in_(user.services)).all(), # user.services, + services=db.query(Service) + .filter(Service.id.in_(user.services)) + .all(), # user.services, status=user.status, data_limit=(user.data_limit or None), expire=(user.expire or None), @@ -289,26 +347,33 @@ def update_user(db: Session, dbuser: User, modify: UserModify): dbuser.status = modify.status if modify.data_limit is not None: - dbuser.data_limit = (modify.data_limit or None) + dbuser.data_limit = modify.data_limit or None if dbuser.status not in (UserStatus.expired, UserStatus.disabled): if not dbuser.data_limit or dbuser.used_traffic < dbuser.data_limit: if dbuser.status != UserStatus.on_hold: dbuser.status = UserStatus.active - if not dbuser.data_limit or (calculate_usage_percent( - dbuser.used_traffic, dbuser.data_limit) < NOTIFY_REACHED_USAGE_PERCENT): - delete_notification_reminder_by_type(db, dbuser.id, ReminderType.data_usage) + if not dbuser.data_limit or ( + calculate_usage_percent(dbuser.used_traffic, dbuser.data_limit) + < NOTIFY_REACHED_USAGE_PERCENT + ): + delete_notification_reminder_by_type( + db, dbuser.id, ReminderType.data_usage + ) else: dbuser.status = UserStatus.limited if modify.expire is not None: - dbuser.expire = (modify.expire or None) + dbuser.expire = modify.expire or None if dbuser.status in (UserStatus.active, UserStatus.expired): if not dbuser.expire or dbuser.expire > datetime.utcnow().timestamp(): dbuser.status = UserStatus.active - if not dbuser.expire or (calculate_expiration_days( - dbuser.expire) > NOTIFY_DAYS_LEFT): - delete_notification_reminder_by_type(db, dbuser.id, ReminderType.expiration_date) + if not dbuser.expire or ( + calculate_expiration_days(dbuser.expire) > NOTIFY_DAYS_LEFT + ): + delete_notification_reminder_by_type( + db, dbuser.id, ReminderType.expiration_date + ) else: dbuser.status = UserStatus.expired @@ -325,7 +390,9 @@ def update_user(db: Session, dbuser: User, modify: UserModify): dbuser.on_hold_expire_duration = modify.on_hold_expire_duration if modify.services is not None: - dbuser.services = db.query(Service).filter(Service.id.in_(modify.services)).all() + dbuser.services = ( + db.query(Service).filter(Service.id.in_(modify.services)).all() + ) dbuser.edit_at = datetime.utcnow() db.commit() @@ -375,7 +442,11 @@ def reset_all_users_data_usage(db: Session, admin: Optional[Admin] = None): for dbuser in query.all(): dbuser.used_traffic = 0 - if dbuser.status not in [UserStatus.on_hold, UserStatus.expired, UserStatus.disabled]: + if dbuser.status not in [ + UserStatus.on_hold, + UserStatus.expired, + UserStatus.disabled, + ]: dbuser.status = UserStatus.active dbuser.usage_logs.clear() dbuser.node_usages.clear() @@ -426,7 +497,7 @@ def create_admin(db: Session, admin: AdminCreate): dbadmin = Admin( username=admin.username, hashed_password=admin.hashed_password, - is_sudo=admin.is_sudo + is_sudo=admin.is_sudo, ) db.add(dbadmin) db.commit() @@ -444,10 +515,15 @@ def update_admin(db: Session, dbadmin: Admin, modified_admin: AdminModify): return dbadmin -def partial_update_admin(db: Session, dbadmin: Admin, modified_admin: AdminPartialModify): +def partial_update_admin( + db: Session, dbadmin: Admin, modified_admin: AdminPartialModify +): if modified_admin.is_sudo is not None: dbadmin.is_sudo = modified_admin.is_sudo - if modified_admin.password is not None and dbadmin.hashed_password != modified_admin.hashed_password: + if ( + modified_admin.password is not None + and dbadmin.hashed_password != modified_admin.hashed_password + ): dbadmin.hashed_password = modified_admin.hashed_password dbadmin.password_reset_at = datetime.utcnow() @@ -462,13 +538,15 @@ def remove_admin(db: Session, dbadmin: Admin): return dbadmin -def get_admins(db: Session, - offset: Optional[int] = None, - limit: Optional[int] = None, - username: Optional[str] = None): +def get_admins( + db: Session, + offset: Optional[int] = None, + limit: Optional[int] = None, + username: Optional[str] = None, +): query = db.query(Admin) if username: - query = query.filter(Admin.username.ilike(f'%{username}%')) + query = query.filter(Admin.username.ilike(f"%{username}%")) if offset: query = query.offset(offset) if limit: @@ -480,7 +558,7 @@ def create_service(db: Session, service: ServiceCreate) -> Service: dbservice = Service( name=service.name, inbounds=db.query(Inbound).filter(Inbound.id.in_(service.inbounds)).all(), - users=db.query(User).filter(User.id.in_(service.users)).all() + users=db.query(User).filter(User.id.in_(service.users)).all(), ) db.add(dbservice) db.commit() @@ -501,7 +579,9 @@ def update_service(db: Session, db_service: Service, modification: ServiceModify db_service.name = modification.name if modification.inbounds is not None: - db_service.inbounds = db.query(Inbound).filter(Inbound.id.in_(modification.inbounds)).all() + db_service.inbounds = ( + db.query(Inbound).filter(Inbound.id.in_(modification.inbounds)).all() + ) db.commit() db.refresh(db_service) @@ -522,9 +602,9 @@ def get_node_by_id(db: Session, node_id: int): return db.query(Node).filter(Node.id == node_id).first() -def get_nodes(db: Session, - status: Optional[Union[NodeStatus, list]] = None, - enabled: bool = None): +def get_nodes( + db: Session, status: Optional[Union[NodeStatus, list]] = None, enabled: bool = None +): query = db.query(Node) if status: @@ -539,21 +619,17 @@ def get_nodes(db: Session, return query.all() -def get_nodes_usage(db: Session, start: datetime, end: datetime) -> List[NodeUsageResponse]: +def get_nodes_usage( + db: Session, start: datetime, end: datetime +) -> List[NodeUsageResponse]: usages = dict() usages[0] = NodeUsageResponse( # Main Core - node_id=None, - node_name="Master", - uplink=0, - downlink=0 + node_id=None, node_name="Master", uplink=0, downlink=0 ) for node in db.query(Node).all(): usages[node.id] = NodeUsageResponse( - node_id=node.id, - node_name=node.name, - uplink=0, - downlink=0 + node_id=node.id, node_name=node.name, uplink=0, downlink=0 ) cond = and_(NodeUsage.created_at >= start, NodeUsage.created_at <= end) @@ -569,10 +645,12 @@ def get_nodes_usage(db: Session, start: datetime, end: datetime) -> List[NodeUsa def create_node(db: Session, node: NodeCreate): - dbnode = Node(name=node.name, - address=node.address, - port=node.port, - connection_backend=node.connection_backend) + dbnode = Node( + name=node.name, + address=node.address, + port=node.port, + connection_backend=node.connection_backend, + ) db.add(dbnode) db.commit() @@ -611,7 +689,13 @@ def update_node(db: Session, dbnode: Node, modify: NodeModify): return dbnode -def update_node_status(db: Session, node_id: int, status: NodeStatus, message: str = None, version: str = None): +def update_node_status( + db: Session, + node_id: int, + status: NodeStatus, + message: str = None, + version: str = None, +): db_node = db.query(Node).where(Node.id == node_id).first() db_node.status = status if message: @@ -621,8 +705,11 @@ def update_node_status(db: Session, node_id: int, status: NodeStatus, message: s def create_notification_reminder( - db: Session, reminder_type: ReminderType, expires_at: datetime, user_id: int) -> NotificationReminder: - reminder = NotificationReminder(type=reminder_type, expires_at=expires_at, user_id=user_id) + db: Session, reminder_type: ReminderType, expires_at: datetime, user_id: int +) -> NotificationReminder: + reminder = NotificationReminder( + type=reminder_type, expires_at=expires_at, user_id=user_id + ) db.add(reminder) db.commit() db.refresh(reminder) @@ -630,11 +717,16 @@ def create_notification_reminder( def get_notification_reminder( - db: Session, user_id: int, reminder_type: ReminderType, + db: Session, + user_id: int, + reminder_type: ReminderType, ) -> Optional[NotificationReminder]: - reminder = db.query(NotificationReminder).filter( - NotificationReminder.user_id == user_id).filter( - NotificationReminder.type == reminder_type).first() + reminder = ( + db.query(NotificationReminder) + .filter(NotificationReminder.user_id == user_id) + .filter(NotificationReminder.type == reminder_type) + .first() + ) if reminder is None: return if reminder.expires_at and reminder.expires_at < datetime.utcnow(): @@ -644,7 +736,9 @@ def get_notification_reminder( return reminder -def delete_notification_reminder_by_type(db: Session, user_id: int, reminder_type: ReminderType) -> None: +def delete_notification_reminder_by_type( + db: Session, user_id: int, reminder_type: ReminderType +) -> None: """Deletes notification reminder filtered by user_id and type if exists""" stmt = delete(NotificationReminder).where( NotificationReminder.user_id == user_id, diff --git a/app/db/migrations/env.py b/app/db/migrations/env.py index 70e30f1..b9115f1 100644 --- a/app/db/migrations/env.py +++ b/app/db/migrations/env.py @@ -11,7 +11,7 @@ from config import SQLALCHEMY_DATABASE_URL # this is the Alembic Config object, which provides # access to the values within the .ini file in use. config = context.config -config.set_main_option('sqlalchemy.url', SQLALCHEMY_DATABASE_URL) +config.set_main_option("sqlalchemy.url", SQLALCHEMY_DATABASE_URL) # Interpret the config file for Python logging. # This line sets up loggers basically. @@ -68,9 +68,7 @@ def run_migrations_online() -> None: ) with connectable.connect() as connection: - context.configure( - connection=connection, target_metadata=target_metadata - ) + context.configure(connection=connection, target_metadata=target_metadata) with context.begin_transaction(): context.run_migrations() diff --git a/app/db/models.py b/app/db/models.py index b2a32f1..61fa3a1 100644 --- a/app/db/models.py +++ b/app/db/models.py @@ -1,18 +1,31 @@ import os from datetime import datetime -from sqlalchemy import (BigInteger, Boolean, Column, DateTime, Enum, - Float, ForeignKey, Integer, String, Table, - UniqueConstraint) +from sqlalchemy import ( + BigInteger, + Boolean, + Column, + DateTime, + Enum, + Float, + ForeignKey, + Integer, + String, + Table, + UniqueConstraint, +) from sqlalchemy.orm import relationship from sqlalchemy.sql.expression import text from app.db.base import Base from app.models.node import NodeStatus -from app.models.proxy import (InboundHostALPN, InboundHostFingerprint, - InboundHostSecurity, ProxyTypes) -from app.models.user import (ReminderType, UserDataLimitResetStrategy, - UserStatus) +from app.models.proxy import ( + InboundHostALPN, + InboundHostFingerprint, + InboundHostSecurity, + ProxyTypes, +) +from app.models.user import ReminderType, UserDataLimitResetStrategy, UserStatus class Admin(Base): @@ -48,7 +61,9 @@ class Service(Base): name = Column(String(64)) # modifications = Column(String(1024)) users = relationship("User", secondary=users_services, back_populates="services") - inbounds = relationship("Inbound", secondary=inbounds_services, back_populates="services") + inbounds = relationship( + "Inbound", secondary=inbounds_services, back_populates="services" + ) class User(Base): @@ -57,20 +72,31 @@ class User(Base): id = Column(Integer, primary_key=True) username = Column(String(32), unique=True, index=True) key = Column(String(64), unique=True) - services = relationship("Service", secondary=users_services, back_populates="users", lazy="joined") + services = relationship( + "Service", secondary=users_services, back_populates="users", lazy="joined" + ) inbounds = relationship( "Inbound", secondary="join(users_services, inbounds_services, inbounds_services.c.service_id == users_services.c.service_id)" - ".join(Inbound, Inbound.id == inbounds_services.c.inbound_id)", viewonly=True, + ".join(Inbound, Inbound.id == inbounds_services.c.inbound_id)", + viewonly=True, distinct_target_key=True, - lazy="joined") + lazy="joined", + ) # proxies = relationship("Proxy", back_populates="user", cascade="all, delete-orphan") status = Column(Enum(UserStatus), nullable=False, default=UserStatus.active) used_traffic = Column(BigInteger, default=0) - node_usages = relationship("NodeUserUsage", back_populates="user", cascade="all,delete,delete-orphan", - lazy="joined") - notification_reminders = relationship("NotificationReminder", back_populates="user", - cascade="all,delete,delete-orphan") + node_usages = relationship( + "NodeUserUsage", + back_populates="user", + cascade="all,delete,delete-orphan", + lazy="joined", + ) + notification_reminders = relationship( + "NotificationReminder", + back_populates="user", + cascade="all,delete,delete-orphan", + ) data_limit = Column(BigInteger) data_limit_reset_strategy = Column( Enum(UserDataLimitResetStrategy), @@ -79,7 +105,9 @@ class User(Base): ) ip_limit = Column(Integer, nullable=False, default=-1) settings = Column(String) - usage_logs = relationship("UserUsageResetLogs", back_populates="user", lazy="joined") + usage_logs = relationship( + "UserUsageResetLogs", back_populates="user", lazy="joined" + ) expire = Column(Integer) admin_id = Column(Integer, ForeignKey("admins.id")) admin = relationship("Admin", back_populates="users") @@ -116,9 +144,7 @@ class UserUsageResetLogs(Base): class Inbound(Base): __tablename__ = "inbounds" - __table_args__ = ( - UniqueConstraint("node_id", "tag"), - ) + __table_args__ = (UniqueConstraint("node_id", "tag"),) id = Column(Integer, primary_key=True) protocol = Column(Enum(ProxyTypes)) @@ -126,7 +152,9 @@ class Inbound(Base): config = Column(String(), nullable=False) node_id = Column(Integer, ForeignKey("nodes.id"), index=True) node = relationship("Node", back_populates="inbounds") - services = relationship("Service", secondary=inbounds_services, back_populates="inbounds") + services = relationship( + "Service", secondary=inbounds_services, back_populates="inbounds" + ) hosts = relationship( "InboundHost", back_populates="inbound", cascade="all, delete, delete-orphan" ) @@ -151,13 +179,13 @@ class InboundHost(Base): Enum(InboundHostALPN), nullable=False, default=InboundHostSecurity.none, - server_default=InboundHostSecurity.none.name + server_default=InboundHostSecurity.none.name, ) fingerprint = Column( Enum(InboundHostFingerprint), nullable=False, default=InboundHostSecurity.none, - server_default=InboundHostSecurity.none.name + server_default=InboundHostSecurity.none.name, ) inbound_id = Column(Integer, ForeignKey("inbounds.id"), nullable=False) @@ -193,9 +221,7 @@ class TLS(Base): class Node(Base): __tablename__ = "nodes" - __table_args__ = ( - UniqueConstraint("address", "port"), - ) + __table_args__ = (UniqueConstraint("address", "port"),) id = Column(Integer, primary_key=True) name = Column(String(256), unique=True) connection_backend = Column(String()) @@ -209,16 +235,20 @@ class Node(Base): created_at = Column(DateTime, default=datetime.utcnow) uplink = Column(BigInteger, default=0) downlink = Column(BigInteger, default=0) - user_usages = relationship("NodeUserUsage", back_populates="node", cascade="all, delete, delete-orphan") - usages = relationship("NodeUsage", back_populates="node", cascade="all, delete, delete-orphan") - usage_coefficient = Column(Float, nullable=False, server_default=text("1.0"), default=1) + user_usages = relationship( + "NodeUserUsage", back_populates="node", cascade="all, delete, delete-orphan" + ) + usages = relationship( + "NodeUsage", back_populates="node", cascade="all, delete, delete-orphan" + ) + usage_coefficient = Column( + Float, nullable=False, server_default=text("1.0"), default=1 + ) class NodeUserUsage(Base): __tablename__ = "node_user_usages" - __table_args__ = ( - UniqueConstraint('created_at', 'user_id', 'node_id'), - ) + __table_args__ = (UniqueConstraint("created_at", "user_id", "node_id"),) id = Column(Integer, primary_key=True) created_at = Column(DateTime, nullable=False) # one hour per record @@ -231,9 +261,7 @@ class NodeUserUsage(Base): class NodeUsage(Base): __tablename__ = "node_usages" - __table_args__ = ( - UniqueConstraint('created_at', 'node_id'), - ) + __table_args__ = (UniqueConstraint("created_at", "node_id"),) id = Column(Integer, primary_key=True) created_at = Column(DateTime, nullable=False) # one hour per record diff --git a/app/dependencies.py b/app/dependencies.py index 469de6d..3614106 100644 --- a/app/dependencies.py +++ b/app/dependencies.py @@ -16,16 +16,18 @@ def get_db(): # Dependency yield db -def get_admin(db: Annotated[Session, Depends(get_db)], - token: Annotated[str, Depends(oauth2_scheme)]): +def get_admin( + db: Annotated[Session, Depends(get_db)], + token: Annotated[str, Depends(oauth2_scheme)], +): payload = get_admin_payload(token) if not payload: return - if payload['username'] in SUDOERS and payload['is_sudo'] is True: - return Admin(username=payload['username'], is_sudo=True) + if payload["username"] in SUDOERS and payload["is_sudo"] is True: + return Admin(username=payload["username"], is_sudo=True) - dbadmin = crud.get_admin(db, payload['username']) + dbadmin = crud.get_admin(db, payload["username"]) if not dbadmin: return @@ -58,7 +60,9 @@ def sudo_admin(admin: Annotated[Admin, Depends(get_current_admin)]): return admin -def get_subscription_user(username: str, key: str, db: Annotated[Session, Depends(get_db)]): +def get_subscription_user( + username: str, key: str, db: Annotated[Session, Depends(get_db)] +): try: int(key, 16) except ValueError: @@ -70,11 +74,15 @@ def get_subscription_user(username: str, key: str, db: Annotated[Session, Depend return db_user -def get_user(username: str, - admin: Annotated[Admin, Depends(get_current_admin)], - db: Annotated[Session, Depends(get_db)]): +def get_user( + username: str, + admin: Annotated[Admin, Depends(get_current_admin)], + db: Annotated[Session, Depends(get_db)], +): db_user = crud.get_user(db, username) - if not (admin.is_sudo or (db_user.admin and db_user.admin.username == admin.username)): + if not ( + admin.is_sudo or (db_user.admin and db_user.admin.username == admin.username) + ): raise HTTPException(status_code=403, detail="You're not allowed") if not db_user: diff --git a/app/marzneshin.py b/app/marzneshin.py index 806c096..62978bf 100644 --- a/app/marzneshin.py +++ b/app/marzneshin.py @@ -15,15 +15,28 @@ from uvicorn import Config, Server import config from app.templates import render_template -from config import (DEBUG, DOCS, HOME_PAGE_TEMPLATE, UVICORN_HOST, - UVICORN_PORT, UVICORN_SSL_CERTFILE, UVICORN_SSL_KEYFILE, - UVICORN_UDS) +from config import ( + DEBUG, + DOCS, + HOME_PAGE_TEMPLATE, + UVICORN_HOST, + UVICORN_PORT, + UVICORN_SSL_CERTFILE, + UVICORN_SSL_KEYFILE, + UVICORN_UDS, +) from . import __version__, telegram from .routes import api_router -from .tasks import (delete_expired_reminders, nodes_startup, - record_realtime_bandwidth, record_user_usages, - reset_user_data_usage, review_users, send_notifications) +from .tasks import ( + delete_expired_reminders, + nodes_startup, + record_realtime_bandwidth, + record_user_usages, + reset_user_data_usage, + review_users, + send_notifications, +) logger = logging.getLogger(__name__) dashboard_path = "/dashboard/" diff --git a/app/marznode/__init__.py b/app/marznode/__init__.py index aa57ae2..c7a9aac 100644 --- a/app/marznode/__init__.py +++ b/app/marznode/__init__.py @@ -1,4 +1,5 @@ """stores nodes and provides entities to communicate with the nodes""" + from typing import Dict from . import operations @@ -9,10 +10,4 @@ from .grpclib import MarzNodeGRPCLIB nodes: Dict[int, MarzNodeBase] = {} -__all__ = [ - "nodes", - "operations", - "MarzNodeGRPCIO", - "MarzNodeGRPCLIB", - "MarzNodeBase" -] +__all__ = ["nodes", "operations", "MarzNodeGRPCIO", "MarzNodeGRPCLIB", "MarzNodeBase"] diff --git a/app/marznode/base.py b/app/marznode/base.py index 961b85b..d7f73a9 100644 --- a/app/marznode/base.py +++ b/app/marznode/base.py @@ -3,8 +3,7 @@ from abc import ABC class MarzNodeBase(ABC): - async def update_user(self, user, - inbounds: list[str] | None = None) -> None: + async def update_user(self, user, inbounds: list[str] | None = None) -> None: """updates a user on the node""" async def fetch_users_stats(self): diff --git a/app/marznode/database.py b/app/marznode/database.py index 5e364e1..ac00079 100644 --- a/app/marznode/database.py +++ b/app/marznode/database.py @@ -6,11 +6,15 @@ from app.models.user import UserStatus class MarzNodeDB: def list_users(self): with GetDB() as db: - relations = crud.get_node_users(db, self.id, [UserStatus.active, UserStatus.on_hold]) + relations = crud.get_node_users( + db, self.id, [UserStatus.active, UserStatus.on_hold] + ) users = dict() for rel in relations: if not users.get(rel[0]): - users[rel[0]] = dict(username=rel[1], id=rel[0], key=rel[2], inbounds=[]) + users[rel[0]] = dict( + username=rel[1], id=rel[0], key=rel[2], inbounds=[] + ) users[rel[0]]["inbounds"].append(rel[3].tag) return list(users.values()) diff --git a/app/marznode/grpcio.py b/app/marznode/grpcio.py index a550edb..8d5202a 100644 --- a/app/marznode/grpcio.py +++ b/app/marznode/grpcio.py @@ -9,7 +9,15 @@ from grpc.aio import insecure_channel from .base import MarzNodeBase from .database import MarzNodeDB from .marznode_pb2_grpc import MarzServiceStub -from .marznode_pb2 import UserData, UsersData, Empty, User, Inbound, XrayLogsRequest, XrayConfig +from .marznode_pb2 import ( + UserData, + UsersData, + Empty, + User, + Inbound, + XrayLogsRequest, + XrayConfig, +) from ..models.node import NodeStatus logger = logging.getLogger(__name__) @@ -21,17 +29,21 @@ channel_options = [ ("grpc.keepalive_permit_without_calls", 1), ("grpc.max_connection_idle_ms", INT_MAX), ("grpc.client_idle_timeout_ms", INT_MAX), - ("grpc.max_connection_age_ms", INT_MAX) + ("grpc.max_connection_age_ms", INT_MAX), ] class MarzNodeGRPCIO(MarzNodeBase, MarzNodeDB): - def __init__(self, node_id: int, address: str, port: int, usage_coefficient: int = 1): + def __init__( + self, node_id: int, address: str, port: int, usage_coefficient: int = 1 + ): self.id = node_id self._address = address self._port = port - self._channel = insecure_channel(f"{self._address}:{self._port}", channel_options) + self._channel = insecure_channel( + f"{self._address}:{self._port}", channel_options + ) self._stub = MarzServiceStub(self._channel) asyncio.create_task(self._monitor_channel()) self._streaming_task = None @@ -73,8 +85,11 @@ class MarzNodeGRPCIO(MarzNodeBase, MarzNodeDB): logger.debug("got something from queue") user = user_update["user"] await stream.write( - UserData(user=User(id=user.id, username=user.username, key=user.key), - inbounds=[Inbound(tag=t) for t in user_update["inbounds"]])) + UserData( + user=User(id=user.id, username=user.username, key=user.key), + inbounds=[Inbound(tag=t) for t in user_update["inbounds"]], + ) + ) async def update_user(self, user, inbounds: set[str] | None = None): if inbounds is None: @@ -84,8 +99,13 @@ class MarzNodeGRPCIO(MarzNodeBase, MarzNodeDB): await self._updates_queue.put({"user": user, "inbounds": inbounds}) async def _repopulate_users(self, users_data: list[dict]) -> None: - updates = [UserData(user=User(id=u["id"], username=u["username"], key=u["key"]), - inbounds=[Inbound(tag=t) for t in u["inbounds"]]) for u in users_data] + updates = [ + UserData( + user=User(id=u["id"], username=u["username"], key=u["key"]), + inbounds=[Inbound(tag=t) for t in u["inbounds"]], + ) + for u in users_data + ] await self._stub.RepopulateUsers(UsersData(users_data=updates)) async def fetch_users_stats(self): @@ -105,7 +125,7 @@ class MarzNodeGRPCIO(MarzNodeBase, MarzNodeDB): async def get_logs(self, include_buffer=True): async for response in self._stub.StreamXrayLogs( - XrayLogsRequest(include_buffer=include_buffer) + XrayLogsRequest(include_buffer=include_buffer) ): yield response.line diff --git a/app/marznode/grpclib.py b/app/marznode/grpclib.py index 42e14fa..1046cb5 100644 --- a/app/marznode/grpclib.py +++ b/app/marznode/grpclib.py @@ -11,21 +11,37 @@ from grpclib.exceptions import StreamTerminatedError from .base import MarzNodeBase from .database import MarzNodeDB from .marznode_grpc import MarzServiceStub -from .marznode_pb2 import UserData, UsersData, Empty, User, Inbound, XrayLogsRequest, XrayConfig +from .marznode_pb2 import ( + UserData, + UsersData, + Empty, + User, + Inbound, + XrayLogsRequest, + XrayConfig, +) from ..models.node import NodeStatus logger = logging.getLogger(__name__) def string_to_temp_file(content: str): - file = tempfile.NamedTemporaryFile(mode='w+t') + file = tempfile.NamedTemporaryFile(mode="w+t") file.write(content) file.flush() return file class MarzNodeGRPCLIB(MarzNodeBase, MarzNodeDB): - def __init__(self, node_id: int, address: str, port: int, ssl_key: str, ssl_cert: str, usage_coefficient: int = 1): + def __init__( + self, + node_id: int, + address: str, + port: int, + ssl_key: str, + ssl_cert: str, + usage_coefficient: int = 1, + ): self.id = node_id self._address = address self._port = port @@ -62,7 +78,9 @@ class MarzNodeGRPCLIB(MarzNodeBase, MarzNodeDB): else: if not self.synced: await self._sync() - self._streaming_task = asyncio.create_task(self._stream_user_updates()) + self._streaming_task = asyncio.create_task( + self._stream_user_updates() + ) self.set_status(NodeStatus.healthy) logger.info("Connected to node %i", self.id) await asyncio.sleep(10) @@ -76,8 +94,11 @@ class MarzNodeGRPCLIB(MarzNodeBase, MarzNodeDB): logger.info("got something from queue") user = user_update["user"] await stream.send_message( - UserData(user=User(id=user.id, username=user.username, key=user.key), - inbounds=[Inbound(tag=t) for t in user_update["inbounds"]])) + UserData( + user=User(id=user.id, username=user.username, key=user.key), + inbounds=[Inbound(tag=t) for t in user_update["inbounds"]], + ) + ) except (OSError, ConnectionError, GRPCError, StreamTerminatedError): logger.info("node %i detached", self.id) self.synced = False @@ -90,8 +111,13 @@ class MarzNodeGRPCLIB(MarzNodeBase, MarzNodeDB): await self._updates_queue.put({"user": user, "inbounds": inbounds}) async def _repopulate_users(self, users_data: list[dict]) -> None: - updates = [UserData(user=User(id=u["id"], username=u["username"], key=u["key"]), - inbounds=[Inbound(tag=t) for t in u["inbounds"]]) for u in users_data] + updates = [ + UserData( + user=User(id=u["id"], username=u["username"], key=u["key"]), + inbounds=[Inbound(tag=t) for t in u["inbounds"]], + ) + for u in users_data + ] await self._stub.RepopulateUsers(UsersData(users_data=updates)) async def fetch_users_stats(self): diff --git a/app/marznode/operations.py b/app/marznode/operations.py index 054b862..815cf4c 100644 --- a/app/marznode/operations.py +++ b/app/marznode/operations.py @@ -51,13 +51,14 @@ async def add_node(db_node, certificate): if db_node.connection_backend == NodeConnectionBackend.grpcio: node = MarzNodeGRPCIO(db_node.id, db_node.address, db_node.port) else: - node = MarzNodeGRPCLIB(db_node.id, db_node.address, db_node.port, certificate.key, - certificate.certificate) + node = MarzNodeGRPCLIB( + db_node.id, + db_node.address, + db_node.port, + certificate.key, + certificate.certificate, + ) marznode.nodes[db_node.id] = node -__all__ = [ - "update_user", - "add_node", - "remove_node" -] +__all__ = ["update_user", "add_node", "remove_node"] diff --git a/app/models/node.py b/app/models/node.py index ced12c6..87d1a61 100644 --- a/app/models/node.py +++ b/app/models/node.py @@ -25,7 +25,9 @@ class NodeBase(BaseModel): name: str address: str port: int = 62050 - connection_backend: NodeConnectionBackend = Field(default=NodeConnectionBackend.grpclib) + connection_backend: NodeConnectionBackend = Field( + default=NodeConnectionBackend.grpclib + ) usage_coefficient: float = Field(ge=0, default=1.0) model_config = ConfigDict(from_attributes=True) @@ -38,14 +40,16 @@ class Node(NodeBase): class NodeCreate(Node): - model_config = ConfigDict(json_schema_extra={ - "example": { - "name": "DE node", - "address": "192.168.1.1", - "port": 62050, - "usage_coefficient": 1 + model_config = ConfigDict( + json_schema_extra={ + "example": { + "name": "DE node", + "address": "192.168.1.1", + "port": 62050, + "usage_coefficient": 1, + } } - }) + ) class NodeModify(Node): @@ -55,16 +59,18 @@ class NodeModify(Node): connection_backend: Optional[NodeConnectionBackend] = Field(None) status: Optional[NodeStatus] = Field(None) usage_coefficient: Optional[float] = Field(None) - model_config = ConfigDict(json_schema_extra={ - "example": { - "name": "DE node", - "address": "192.168.1.1", - "port": 62050, - "api_port": 62051, - "status": "disabled", - "usage_coefficient": 1.0 + model_config = ConfigDict( + json_schema_extra={ + "example": { + "name": "DE node", + "address": "192.168.1.1", + "port": 62050, + "api_port": 62051, + "status": "disabled", + "usage_coefficient": 1.0, + } } - }) + ) class NodeResponse(Node): diff --git a/app/models/proxy.py b/app/models/proxy.py index 1b538b4..670d01f 100644 --- a/app/models/proxy.py +++ b/app/models/proxy.py @@ -1,20 +1,21 @@ import json from enum import Enum from typing import Optional, Union, List + # from uuid import UUID, uuid4 from pydantic import ConfigDict, BaseModel, Field, validator class XTLSFlows(Enum): - NONE = '' - VISION = 'xtls-rprx-vision' + NONE = "" + VISION = "xtls-rprx-vision" class ShadowsocksMethods(Enum): - AES_128_GCM = 'aes-128-gcm' - AES_256_GCM = 'aes-256-gcm' - CHACHA20_POLY1305 = 'chacha20-ietf-poly1305' + AES_128_GCM = "aes-128-gcm" + AES_256_GCM = "aes-256-gcm" + CHACHA20_POLY1305 = "chacha20-ietf-poly1305" class ProxyTypes(str, Enum): diff --git a/app/models/service.py b/app/models/service.py index 7919aed..9bae7f0 100644 --- a/app/models/service.py +++ b/app/models/service.py @@ -21,24 +21,24 @@ class Service(ServiceBase): class ServiceCreate(ServiceBase): users: List[int] = Field([]) inbounds: List[int] = Field([]) - model_config = ConfigDict(json_schema_extra={ - "example": { - "name": "my service 1", - "inbounds": [1, 5, 9], - "users": [1, 2, 3] + model_config = ConfigDict( + json_schema_extra={ + "example": { + "name": "my service 1", + "inbounds": [1, 5, 9], + "users": [1, 2, 3], + } } - }) + ) class ServiceModify(ServiceBase): inbounds: Optional[List[int]] = Field(None) - model_config = ConfigDict(json_schema_extra={ - "example": { - "id": 2, - "name": "my service 2", - "inbounds": [1, 2, 3] + model_config = ConfigDict( + json_schema_extra={ + "example": {"id": 2, "name": "my service 2", "inbounds": [1, 2, 3]} } - }) + ) class ServiceResponse(Service): diff --git a/app/models/user.py b/app/models/user.py index a34bf40..b7114bf 100644 --- a/app/models/user.py +++ b/app/models/user.py @@ -4,8 +4,17 @@ from datetime import datetime from enum import Enum from typing import List, Optional, Union, Annotated -from pydantic import field_validator, ConfigDict, BaseModel, Field, validator, computed_field, ValidationInfo, constr, \ - StringConstraints +from pydantic import ( + field_validator, + ConfigDict, + BaseModel, + Field, + validator, + computed_field, + ValidationInfo, + constr, + StringConstraints, +) from config import XRAY_SUBSCRIPTION_URL_PREFIX @@ -87,9 +96,10 @@ class UserBase(BaseModel): @classmethod def validate_timeout(cls, v: Optional[int]): # Check if expire is 0 or None and timeout is not 0 or None - if (v in (0, None)): + if v in (0, None): return None return v + model_config = ConfigDict(from_attributes=True) @@ -104,25 +114,27 @@ class User(UserBase): class UserCreate(User): status: UserStatusCreate = Field(None, validate_default=True) services: List[int] = Field([]) - model_config = ConfigDict(json_schema_extra={ - "example": { - "username": "user1234", - "services": [1, 2, 3], - "expire": 0, - "data_limit": 0, - "data_limit_reset_strategy": "no_reset", - "status": "active", - "note": "", - "on_hold_timeout": "2023-11-03T20:30:00", - "on_hold_expire_duration": 0, + model_config = ConfigDict( + json_schema_extra={ + "example": { + "username": "user1234", + "services": [1, 2, 3], + "expire": 0, + "data_limit": 0, + "data_limit_reset_strategy": "no_reset", + "status": "active", + "note": "", + "on_hold_timeout": "2023-11-03T20:30:00", + "on_hold_expire_duration": 0, + } } - }) + ) # TODO[pydantic]: We couldn't refactor the `validator`, please replace it by `field_validator` manually. # Check https://docs.pydantic.dev/dev-v2/migration/#changes-to-validators for more information. - #@validator("status", pre=True, always=True) - #def validate_status(cls, value): - + # @validator("status", pre=True, always=True) + # def validate_status(cls, value): + # TODO[pydantic]: We couldn't refactor the `validator`, please replace it by `field_validator` manually. # Check https://docs.pydantic.dev/dev-v2/migration/#changes-to-validators for more information. @field_validator("status", mode="before") @@ -133,7 +145,9 @@ class UserCreate(User): expire = info.data.get("expire") if status == UserStatusCreate.on_hold: if on_hold_expire == 0 or on_hold_expire is None: - raise ValueError("User cannot be on hold without a valid on_hold_expire_duration.") + raise ValueError( + "User cannot be on hold without a valid on_hold_expire_duration." + ) if expire: raise ValueError("User cannot be on hold with specified expire.") return status @@ -143,20 +157,21 @@ class UserModify(User): status: Optional[UserStatusModify] = Field(None) services: List[int] = Field([]) data_limit_reset_strategy: Optional[UserDataLimitResetStrategy] = Field(None) - model_config = ConfigDict(json_schema_extra={ - "example": { - "username": "mammad1234", - "services": [1, 2, 3], - "expire": 0, - "data_limit": 0, - "data_limit_reset_strategy": "no_reset", - "status": "active", - "note": "", - "on_hold_timeout": "2023-11-03T20:30:00", - "on_hold_expire_duration": 0, + model_config = ConfigDict( + json_schema_extra={ + "example": { + "username": "mammad1234", + "services": [1, 2, 3], + "expire": 0, + "data_limit": 0, + "data_limit_reset_strategy": "no_reset", + "status": "active", + "note": "", + "on_hold_timeout": "2023-11-03T20:30:00", + "on_hold_expire_duration": 0, + } } - }) - + ) # TODO[pydantic]: We couldn't refactor the `validator`, please replace it by `field_validator` manually. # Check https://docs.pydantic.dev/dev-v2/migration/#changes-to-validators for more information. @@ -166,7 +181,9 @@ class UserModify(User): expire = values.get("expire") if status == UserStatusCreate.on_hold: if not on_hold_expire: - raise ValueError("User cannot be on hold without a valid on_hold_expire_duration.") + raise ValueError( + "User cannot be on hold without a valid on_hold_expire_duration." + ) if expire: raise ValueError("User cannot be on hold with specified expire.") return status @@ -194,7 +211,7 @@ class UserResponse(User): @property def subscription_url(self) -> str: salt = secrets.token_hex(8) - url_prefix = (XRAY_SUBSCRIPTION_URL_PREFIX).replace('*', salt) + url_prefix = (XRAY_SUBSCRIPTION_URL_PREFIX).replace("*", salt) return f"{url_prefix}/sub/{self.username}/{self.key}" diff --git a/app/routes/__init__.py b/app/routes/__init__.py index a0cdb36..c54208c 100644 --- a/app/routes/__init__.py +++ b/app/routes/__init__.py @@ -12,6 +12,4 @@ api_router.include_router(subscription.router) api_router.include_router(system.router, prefix="/api") api_router.include_router(user.router, prefix="/api") -__all__ = [ - "api_router" -] +__all__ = ["api_router"] diff --git a/app/routes/admin.py b/app/routes/admin.py index b9b38d0..a613e7b 100644 --- a/app/routes/admin.py +++ b/app/routes/admin.py @@ -29,23 +29,21 @@ def authenticate_admin(db: Session, username: str, password: str) -> Optional[Ad if not dbadmin: return None - return dbadmin if AdminInDB.model_validate(dbadmin).verify_password(password) else None + return ( + dbadmin if AdminInDB.model_validate(dbadmin).verify_password(password) else None + ) @router.get("", response_model=Page[Admin]) -def get_admins(db: DBDep, - admin: SudoAdminDep, - username: str = None): +def get_admins(db: DBDep, admin: SudoAdminDep, username: str = None): query = db.query(DBAdmin) if username: - query = query.filter(DBAdmin.username.ilike(f'%{username}%')) + query = query.filter(DBAdmin.username.ilike(f"%{username}%")) return paginate(db, query) @router.post("", response_model=Admin) -def create_admin(new_admin: AdminCreate, - db: DBDep, - admin: SudoAdminDep): +def create_admin(new_admin: AdminCreate, db: DBDep, admin: SudoAdminDep): try: dbadmin = crud.create_admin(db, new_admin) except sqlalchemy.exc.IntegrityError: @@ -61,13 +59,14 @@ def get_current_admin(admin: AdminDep): @router.post("/token", response_model=Token) -def admin_token(form_data: Annotated[OAuth2PasswordRequestForm, Depends()], - db: DBDep): +def admin_token(form_data: Annotated[OAuth2PasswordRequestForm, Depends()], db: DBDep): if authenticate_env_sudo(form_data.username, form_data.password): return Token(access_token=create_admin_token(form_data.username, is_sudo=True)) if dbadmin := authenticate_admin(db, form_data.username, form_data.password): - return Token(access_token=create_admin_token(form_data.username, is_sudo=dbadmin.is_sudo)) + return Token( + access_token=create_admin_token(form_data.username, is_sudo=dbadmin.is_sudo) + ) raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, @@ -77,10 +76,9 @@ def admin_token(form_data: Annotated[OAuth2PasswordRequestForm, Depends()], @router.put("/{username}", response_model=Admin) -def modify_admin(username: str, - modified_admin: AdminModify, - db: DBDep, - admin: AdminDep): +def modify_admin( + username: str, modified_admin: AdminModify, db: DBDep, admin: AdminDep +): if not (admin.is_sudo or admin.username == username): raise HTTPException(status_code=403, detail="You're not allowed") @@ -104,9 +102,7 @@ def modify_admin(username: str, @router.delete("/{username}") -def remove_admin(username: str, - db: DBDep, - admin: SudoAdminDep): +def remove_admin(username: str, db: DBDep, admin: SudoAdminDep): dbadmin = crud.get_admin(db, username) if not dbadmin: raise HTTPException(status_code=404, detail="Admin not found") diff --git a/app/routes/inbounds.py b/app/routes/inbounds.py index 853f941..da88aca 100644 --- a/app/routes/inbounds.py +++ b/app/routes/inbounds.py @@ -8,7 +8,9 @@ from app.db.models import InboundHost as DBInboundHost, Inbound as DBInbound from app.dependencies import DBDep, sudo_admin from app.models.proxy import Inbound, InboundHost, InboundHostResponse -router = APIRouter(prefix="/inbounds", dependencies=[Depends(sudo_admin)], tags=['Inbounds']) +router = APIRouter( + prefix="/inbounds", dependencies=[Depends(sudo_admin)], tags=["Inbounds"] +) @router.get("", response_model=Page[Inbound]) @@ -23,7 +25,7 @@ def get_inbounds(db: DBDep, tag: str = Query(None)): return paginate(db, query) -@router.get('/hosts', response_model=Page[InboundHost]) +@router.get("/hosts", response_model=Page[InboundHost]) def get_hosts(db: DBDep): return paginate(db.query(DBInboundHost)) diff --git a/app/routes/node.py b/app/routes/node.py index 2ac7d2e..058eab7 100644 --- a/app/routes/node.py +++ b/app/routes/node.py @@ -13,18 +13,26 @@ from app.db import crud, get_tls_certificate from app.db.models import Node from app.dependencies import DBDep, SudoAdminDep, EndDateDep, StartDateDep, get_admin -from app.models.node import (NodeCreate, NodeModify, NodeResponse, - NodeSettings, NodeStatus, NodesUsageResponse) +from app.models.node import ( + NodeCreate, + NodeModify, + NodeResponse, + NodeSettings, + NodeStatus, + NodesUsageResponse, +) logger = logging.getLogger(__name__) -router = APIRouter(prefix="/nodes", tags=['Node']) +router = APIRouter(prefix="/nodes", tags=["Node"]) @router.get("", response_model=Page[NodeResponse]) -def get_nodes(db: DBDep, - admin: SudoAdminDep, - status: list[NodeStatus] = Query(None), - name: str = Query(None)): +def get_nodes( + db: DBDep, + admin: SudoAdminDep, + status: list[NodeStatus] = Query(None), + name: str = Query(None), +): query = db.query(Node) if name: @@ -37,14 +45,14 @@ def get_nodes(db: DBDep, @router.post("", response_model=NodeResponse) -async def add_node(new_node: NodeCreate, - db: DBDep, - admin: SudoAdminDep): +async def add_node(new_node: NodeCreate, db: DBDep, admin: SudoAdminDep): try: db_node = crud.create_node(db, new_node) except sqlalchemy.exc.IntegrityError: db.rollback() - raise HTTPException(status_code=409, detail=f"Node \"{new_node.name}\" already exists") + raise HTTPException( + status_code=409, detail=f'Node "{new_node.name}" already exists' + ) certificate = get_tls_certificate(db) await marznode.operations.add_node(db_node, certificate) @@ -54,20 +62,16 @@ async def add_node(new_node: NodeCreate, @router.get("/settings", response_model=NodeSettings) -def get_node_settings(db: DBDep, - admin: SudoAdminDep): +def get_node_settings(db: DBDep, admin: SudoAdminDep): tls = crud.get_tls_certificate(db) - return NodeSettings( - certificate=tls.certificate - ) + return NodeSettings(certificate=tls.certificate) @router.get("/usage", response_model=NodesUsageResponse) -def get_usage(db: DBDep, - admin: SudoAdminDep, - start_date: StartDateDep, - end_date: EndDateDep): +def get_usage( + db: DBDep, admin: SudoAdminDep, start_date: StartDateDep, end_date: EndDateDep +): """ Get nodes usage """ @@ -77,9 +81,7 @@ def get_usage(db: DBDep, @router.get("/{node_id}", response_model=NodeResponse) -def get_node(node_id: int, - db: DBDep, - admin: SudoAdminDep): +def get_node(node_id: int, db: DBDep, admin: SudoAdminDep): db_node = crud.get_node_by_id(db, node_id) if not db_node: raise HTTPException(status_code=404, detail="Node not found") @@ -88,15 +90,12 @@ def get_node(node_id: int, @router.websocket("/{node_id}/logs") -async def node_logs(node_id: int, - websocket: WebSocket, - db: DBDep, - include_buffer: bool = True): - token = ( - websocket.query_params.get('token') - or - websocket.headers.get('Authorization', '').removeprefix("Bearer ") - ) +async def node_logs( + node_id: int, websocket: WebSocket, db: DBDep, include_buffer: bool = True +): + token = websocket.query_params.get("token") or websocket.headers.get( + "Authorization", "" + ).removeprefix("Bearer ") admin = get_admin(db, token) if not admin or not admin.is_sudo: @@ -114,10 +113,9 @@ async def node_logs(node_id: int, @router.put("/{node_id}", response_model=NodeResponse) -async def modify_node(node_id: int, - modified_node: NodeModify, - db: DBDep, - admin: SudoAdminDep): +async def modify_node( + node_id: int, modified_node: NodeModify, db: DBDep, admin: SudoAdminDep +): db_node = crud.get_node_by_id(db, node_id) if not db_node: raise HTTPException(status_code=404, detail="Node not found") @@ -134,9 +132,7 @@ async def modify_node(node_id: int, @router.delete("/{node_id}") -async def remove_node(node_id: int, - db: DBDep, - admin: SudoAdminDep): +async def remove_node(node_id: int, db: DBDep, admin: SudoAdminDep): db_node = crud.get_node_by_id(db, node_id) if not db_node: raise HTTPException(status_code=404, detail="Node not found") @@ -149,9 +145,7 @@ async def remove_node(node_id: int, @router.post("/{node_id}/resync") -async def reconnect_node(node_id: int, - db: DBDep, - admin: SudoAdminDep): +async def reconnect_node(node_id: int, db: DBDep, admin: SudoAdminDep): db_node = crud.get_node_by_id(db, node_id) if not db_node: raise HTTPException(status_code=404, detail="Node not found") @@ -160,19 +154,16 @@ async def reconnect_node(node_id: int, @router.get("/{node_id}/xray_config") -async def get_node_xray_config(node_id: int, - db: DBDep, - admin: SudoAdminDep): +async def get_node_xray_config(node_id: int, db: DBDep, admin: SudoAdminDep): if not (node := marznode.nodes.get(node_id)): raise HTTPException(status_code=404, detail="Node not found") return json.loads(await node.get_xray_config()) @router.put("/{node_id}/xray_config") -async def alter_node_xray_config(node_id: int, - db: DBDep, - admin: SudoAdminDep, - body: Annotated[dict, Body()]): +async def alter_node_xray_config( + node_id: int, db: DBDep, admin: SudoAdminDep, body: Annotated[dict, Body()] +): if not (node := marznode.nodes.get(node_id)): raise HTTPException(status_code=404, detail="Node not found") xray_config = json.dumps(body) diff --git a/app/routes/service.py b/app/routes/service.py index 840c5b5..a990a0e 100644 --- a/app/routes/service.py +++ b/app/routes/service.py @@ -9,10 +9,11 @@ from app import marznode from app.db import crud from app.db.models import Service from app.dependencies import DBDep, sudo_admin -from app.models.service import (ServiceCreate, ServiceModify, - ServiceResponse) +from app.models.service import ServiceCreate, ServiceModify, ServiceResponse -router = APIRouter(prefix="/services", dependencies=[Depends(sudo_admin)], tags=['Service']) +router = APIRouter( + prefix="/services", dependencies=[Depends(sudo_admin)], tags=["Service"] +) @router.get("", response_model=Page[ServiceResponse]) @@ -24,8 +25,7 @@ def get_services(db: DBDep, name: str = Query(None)): @router.post("", response_model=ServiceResponse) -def add_service(new_service: ServiceCreate, - db: DBDep): +def add_service(new_service: ServiceCreate, db: DBDep): """ Add a new service @@ -36,7 +36,9 @@ def add_service(new_service: ServiceCreate, return crud.create_service(db, new_service) except sqlalchemy.exc.IntegrityError: db.rollback() - raise HTTPException(status_code=409, detail="Service by this name already exists") + raise HTTPException( + status_code=409, detail="Service by this name already exists" + ) @router.get("/{id}", response_model=ServiceResponse) @@ -52,9 +54,7 @@ def get_service(id: int, db: DBDep): @router.put("/{id}", response_model=ServiceResponse) -async def modify_service(id: int, - modification: ServiceModify, - db: DBDep): +async def modify_service(id: int, modification: ServiceModify, db: DBDep): """ Modify Service @@ -65,7 +65,7 @@ async def modify_service(id: int, # TODO: Update all affected users in nodes dbservice = crud.get_service(db, id) if not dbservice: - raise HTTPException(status_code=404, detail="Service not found") + raise HTTPException(status_code=404, detail="Service not found") old_inbounds = {(i.node_id, i.protocol, i.tag) for i in dbservice.inbounds} try: response = crud.update_service(db, dbservice, modification) @@ -79,11 +79,10 @@ async def modify_service(id: int, @router.delete("/{id}") -def remove_service(id: int, - db: DBDep): +def remove_service(id: int, db: DBDep): dbservice = crud.get_service(db, id) if not dbservice: raise HTTPException(status_code=404, detail="Service not found") - + crud.remove_service(db, dbservice) return dict() diff --git a/app/routes/subscription.py b/app/routes/subscription.py index 5d3bfce..a64d353 100644 --- a/app/routes/subscription.py +++ b/app/routes/subscription.py @@ -13,10 +13,10 @@ from config import ( SUB_PROFILE_TITLE, SUB_SUPPORT_URL, SUB_UPDATE_INTERVAL, - SUBSCRIPTION_PAGE_TEMPLATE + SUBSCRIPTION_PAGE_TEMPLATE, ) -router = APIRouter(prefix="/sub", tags=['Subscription']) +router = APIRouter(prefix="/sub", tags=["Subscription"]) def get_subscription_user_info(user: UserResponse) -> dict: @@ -29,10 +29,12 @@ def get_subscription_user_info(user: UserResponse) -> dict: @router.get("/{username}/{key}") -def user_subscription(db_user: SubUserDep, - request: Request, - db: DBDep, - user_agent: str = Header(default="")): +def user_subscription( + db_user: SubUserDep, + request: Request, + db: DBDep, + user_agent: str = Header(default=""), +): """ Subscription link, V2ray and Clash supported """ @@ -41,16 +43,15 @@ def user_subscription(db_user: SubUserDep, user: UserResponse = UserResponse.model_validate(db_user) if "text/html" in accept_header: - links = generate_v2ray_links(user.inbounds, - user.key, - user.model_dump( - exclude={"subscription_url", "services", "inbounds", "links"} - )) + links = generate_v2ray_links( + user.inbounds, + user.key, + user.model_dump( + exclude={"subscription_url", "services", "inbounds", "links"} + ), + ) return HTMLResponse( - render_template( - SUBSCRIPTION_PAGE_TEMPLATE, - {"user": user, "links": links} - ) + render_template(SUBSCRIPTION_PAGE_TEMPLATE, {"user": user, "links": links}) ) response_headers = { @@ -63,26 +64,36 @@ def user_subscription(db_user: SubUserDep, f"{key}={val}" for key, val in get_subscription_user_info(user).items() if val is not None - ) + ), } crud.update_user_sub(db, db_user, user_agent) - if re.match('^([Cc]lash-verge|[Cc]lash-?[Mm]eta)', user_agent): - conf = generate_subscription(user=user, config_format="clash-meta", as_base64=False) + if re.match("^([Cc]lash-verge|[Cc]lash-?[Mm]eta)", user_agent): + conf = generate_subscription( + user=user, config_format="clash-meta", as_base64=False + ) return Response(content=conf, media_type="text/yaml", headers=response_headers) - elif re.match('^([Cc]lash|[Ss]tash)', user_agent): + elif re.match("^([Cc]lash|[Ss]tash)", user_agent): conf = generate_subscription(user=user, config_format="clash", as_base64=False) return Response(content=conf, media_type="text/yaml", headers=response_headers) - elif re.match('^(SFA|SFI|SFM|SFT)', user_agent): - conf = generate_subscription(user=user, config_format="sing-box", as_base64=False) - return Response(content=conf, media_type="application/json", headers=response_headers) + elif re.match("^(SFA|SFI|SFM|SFT)", user_agent): + conf = generate_subscription( + user=user, config_format="sing-box", as_base64=False + ) + return Response( + content=conf, media_type="application/json", headers=response_headers + ) - elif re.match('^(SS|SSR|SSD|SSS|Outline|Shadowsocks|SSconf)', user_agent): - conf = generate_subscription(user=user, config_format="outline", as_base64=False) - return Response(content=conf, media_type="application/json", headers=response_headers) + elif re.match("^(SS|SSR|SSD|SSS|Outline|Shadowsocks|SSconf)", user_agent): + conf = generate_subscription( + user=user, config_format="outline", as_base64=False + ) + return Response( + content=conf, media_type="application/json", headers=response_headers + ) else: conf = generate_subscription(user=user, config_format="v2ray", as_base64=True) @@ -95,10 +106,9 @@ def user_subscription_info(db_user: SubUserDep): @router.get("/{username}/{key}/usage") -def user_get_usage(db_user: SubUserDep, - db: DBDep, - start_date: StartDateDep, - end_date: EndDateDep): +def user_get_usage( + db_user: SubUserDep, db: DBDep, start_date: StartDateDep, end_date: EndDateDep +): usages = crud.get_user_usages(db, db_user, start_date, end_date) return {"usages": usages, "username": db_user.username} @@ -106,9 +116,10 @@ def user_get_usage(db_user: SubUserDep, @router.get("/{username}/{key}/{client_type}") def user_subscription_with_client_type( - db_user: SubUserDep, - request: Request, - client_type: str = Path(..., regex="sing-box|clash-meta|clash|outline|v2ray")): + db_user: SubUserDep, + request: Request, + client_type: str = Path(..., regex="sing-box|clash-meta|clash|outline|v2ray"), +): """ Subscription link, v2ray, clash, sing-box, outline and clash-meta supported """ @@ -125,16 +136,22 @@ def user_subscription_with_client_type( f"{key}={val}" for key, val in get_subscription_user_info(user).items() if val is not None - ) + ), } if client_type == "clash-meta": - conf = generate_subscription(user=user, config_format="clash-meta", as_base64=False) + conf = generate_subscription( + user=user, config_format="clash-meta", as_base64=False + ) return Response(content=conf, media_type="text/yaml", headers=response_headers) elif client_type == "sing-box": - conf = generate_subscription(user=user, config_format="sing-box", as_base64=False) - return Response(content=conf, media_type="application/json", headers=response_headers) + conf = generate_subscription( + user=user, config_format="sing-box", as_base64=False + ) + return Response( + content=conf, media_type="application/json", headers=response_headers + ) elif client_type == "clash": conf = generate_subscription(user=user, config_format="clash", as_base64=False) @@ -145,8 +162,12 @@ def user_subscription_with_client_type( return Response(content=conf, media_type="text/plain", headers=response_headers) elif client_type == "outline": - conf = generate_subscription(user=user, config_format="outline", as_base64=False) - return Response(content=conf, media_type="application/json", headers=response_headers) + conf = generate_subscription( + user=user, config_format="outline", as_base64=False + ) + return Response( + content=conf, media_type="application/json", headers=response_headers + ) else: raise HTTPException(status_code=400, detail="Invalid subscription type") diff --git a/app/routes/system.py b/app/routes/system.py index d86d23a..b7bd746 100644 --- a/app/routes/system.py +++ b/app/routes/system.py @@ -21,7 +21,9 @@ def get_system_stats(db: DBDep, admin: AdminDep): dbadmin: Union[Admin, None] = crud.get_admin(db, admin.username) total_user = crud.get_users_count(db, admin=dbadmin if not admin.is_sudo else None) - users_active = crud.get_users_count(db, status=UserStatus.active, admin=dbadmin if not admin.is_sudo else None) + users_active = crud.get_users_count( + db, status=UserStatus.active, admin=dbadmin if not admin.is_sudo else None + ) realtime_bandwidth_stats = realtime_bandwidth() return SystemStats( diff --git a/app/routes/user.py b/app/routes/user.py index 73a17d3..6b326d6 100644 --- a/app/routes/user.py +++ b/app/routes/user.py @@ -11,13 +11,25 @@ from fastapi_pagination.links import Page from app import marznode from app.db import crud, User -from app.dependencies import DBDep, AdminDep, SudoAdminDep, UserDep, StartDateDep, EndDateDep -from app.models.user import (UserCreate, UserModify, UserResponse, - UserStatus, UserUsagesResponse) +from app.dependencies import ( + DBDep, + AdminDep, + SudoAdminDep, + UserDep, + StartDateDep, + EndDateDep, +) +from app.models.user import ( + UserCreate, + UserModify, + UserResponse, + UserStatus, + UserUsagesResponse, +) from app.utils import report logger = logging.getLogger(__name__) -router = APIRouter(prefix="/users", tags=['User']) +router = APIRouter(prefix="/users", tags=["User"]) class UsersSortingOptions(str, Enum): @@ -29,12 +41,14 @@ class UsersSortingOptions(str, Enum): @router.get("", response_model=Page[UserResponse]) -def get_users(db: DBDep, - admin: AdminDep, - username: list[str] = Query(None), - status: list[UserStatus] = Query(None), - order_by: UsersSortingOptions = Query(None), - descending: bool = Query(False)): +def get_users( + db: DBDep, + admin: AdminDep, + username: list[str] = Query(None), + status: list[UserStatus] = Query(None), + order_by: UsersSortingOptions = Query(None), + descending: bool = Query(False), +): """ Filters users based on the options """ @@ -66,9 +80,7 @@ def get_users(db: DBDep, @router.post("", response_model=UserResponse) -async def add_user(new_user: UserCreate, - db: DBDep, - admin: AdminDep): +async def add_user(new_user: UserCreate, db: DBDep, admin: AdminDep): """ Add a new user @@ -80,27 +92,22 @@ async def add_user(new_user: UserCreate, # TODO expire should be datetime instead of timestamp try: - db_user = crud.create_user(db, new_user, - admin=crud.get_admin(db, admin.username)) + db_user = crud.create_user( + db, new_user, admin=crud.get_admin(db, admin.username) + ) except sqlalchemy.exc.IntegrityError: db.rollback() raise HTTPException(status_code=409, detail="User already exists") user = UserResponse.model_validate(db_user) await marznode.operations.update_user(user=db_user) - asyncio.create_task( - report.user_created( - user=user, - user_id=db_user.id, - by=admin - )) + asyncio.create_task(report.user_created(user=user, user_id=db_user.id, by=admin)) logger.info("New user `%s` added", db_user.username) return user @router.post("/reset") -async def reset_users_data_usage(db: DBDep, - admin: SudoAdminDep): +async def reset_users_data_usage(db: DBDep, admin: SudoAdminDep): """ Reset all users data usage, You will need to restart for this to take effect for now @@ -111,9 +118,7 @@ async def reset_users_data_usage(db: DBDep, @router.delete("/expired") -def delete_expired(passed_time: int, - db: DBDep, - admin: AdminDep): +def delete_expired(passed_time: int, db: DBDep, admin: AdminDep): """ Delete expired users - **passed_time** must be a timestamp @@ -122,25 +127,26 @@ def delete_expired(passed_time: int, dbadmin = crud.get_admin(db, admin.username) - db_users = crud.get_users(db=db, - status=[UserStatus.expired, UserStatus.limited], - admin=dbadmin if not admin.is_sudo else None) + db_users = crud.get_users( + db=db, + status=[UserStatus.expired, UserStatus.limited], + admin=dbadmin if not admin.is_sudo else None, + ) current_time = int(datetime.now(timezone.utc).timestamp()) expiration_threshold = current_time - passed_time expired_users = [ - user for user in db_users if user.expire is not None and user.expire <= expiration_threshold] + user + for user in db_users + if user.expire is not None and user.expire <= expiration_threshold + ] if not expired_users: raise HTTPException(status_code=404, detail="No expired user found.") for db_user in expired_users: crud.remove_user(db, db_user) - asyncio.create_task( - report.user_deleted( - username=db_user.username, - by=admin - )) + asyncio.create_task(report.user_deleted(username=db_user.username, by=admin)) logger.info("User `%s` removed", db_user.username) @@ -156,10 +162,9 @@ def get_user(db_user: UserDep): @router.put("/{username}", response_model=UserResponse) -async def modify_user(db_user: UserDep, - modifications: UserModify, - db: DBDep, - admin: AdminDep): +async def modify_user( + db_user: UserDep, modifications: UserModify, db: DBDep, admin: AdminDep +): """ Modify a user @@ -167,9 +172,13 @@ async def modify_user(db_user: UserDep, - set **data_limit** to 0 to make the user unlimited in data, null for no change """ old_status = db_user.status - status_change = bool(modifications.status and modifications.status != db_user.status) - if status_change and (modifications.status == UserStatus.disabled and - old_status in {UserStatus.active, UserStatus.on_hold}): + status_change = bool( + modifications.status and modifications.status != db_user.status + ) + if status_change and ( + modifications.status == UserStatus.disabled + and old_status in {UserStatus.active, UserStatus.on_hold} + ): await marznode.operations.remove_user(db_user) old_inbounds = {(i.node_id, i.protocol, i.tag) for i in db_user.inbounds} @@ -181,31 +190,29 @@ async def modify_user(db_user: UserDep, if inbound_change and user.status in {UserStatus.active, UserStatus.on_hold}: await marznode.operations.update_user(new_user, old_inbounds) - asyncio.create_task(report.user_updated( - user=user, - by=admin)) - + asyncio.create_task(report.user_updated(user=user, by=admin)) + logger.info("User `%s` modified", user.username) if status_change: - asyncio.create_task(report.status_change( - username=user.username, - status=user.status, - user=user, - by=admin)) - - logger.info("User `%s` status changed from `%s` to `%s`", - user.username, - old_status, - user.status) + asyncio.create_task( + report.status_change( + username=user.username, status=user.status, user=user, by=admin + ) + ) + + logger.info( + "User `%s` status changed from `%s` to `%s`", + user.username, + old_status, + user.status, + ) return user @router.delete("/{username}") -async def remove_user(db_user: UserDep, - db: DBDep, - admin: AdminDep): +async def remove_user(db_user: UserDep, db: DBDep, admin: AdminDep): """ Remove a user """ @@ -214,19 +221,13 @@ async def remove_user(db_user: UserDep, crud.remove_user(db, db_user) db.flush() - asyncio.create_task( - report.user_deleted( - username=db_user.username, - by=admin - )) + asyncio.create_task(report.user_deleted(username=db_user.username, by=admin)) logger.info("User %s deleted", db_user.username) return {} @router.post("/{username}/reset", response_model=UserResponse) -async def reset_user_data_usage(db_user: UserDep, - db: DBDep, - admin: AdminDep): +async def reset_user_data_usage(db_user: UserDep, db: DBDep, admin: AdminDep): """ Reset user data usage """ @@ -237,9 +238,7 @@ async def reset_user_data_usage(db_user: UserDep, await marznode.operations.update_user(db_user) user = UserResponse.model_validate(db_user) - asyncio.create_task(report.user_data_usage_reset( - user=user, - by=admin)) + asyncio.create_task(report.user_data_usage_reset(user=user, by=admin)) logger.info("User `%s`'s usage was reset", db_user.username) @@ -247,9 +246,7 @@ async def reset_user_data_usage(db_user: UserDep, @router.post("/{username}/revoke_sub", response_model=UserResponse) -async def revoke_user_subscription(db_user: UserDep, - db: DBDep, - admin: AdminDep): +async def revoke_user_subscription(db_user: UserDep, db: DBDep, admin: AdminDep): """ Revoke users subscription (Subscription link and proxies) """ @@ -259,11 +256,7 @@ async def revoke_user_subscription(db_user: UserDep, await marznode.operations.remove_user(db_user) await marznode.operations.update_user(db_user) user = UserResponse.model_validate(db_user) - asyncio.create_task( - report.user_subscription_revoked( - user=user, - by=admin - )) + asyncio.create_task(report.user_subscription_revoked(user=user, by=admin)) logger.info("User %s subscription revoked", db_user.username) @@ -271,10 +264,9 @@ async def revoke_user_subscription(db_user: UserDep, @router.get("/{username}/usage", response_model=UserUsagesResponse) -def get_user_usage(db: DBDep, - db_user: UserDep, - start_date: StartDateDep, - end_date: EndDateDep): +def get_user_usage( + db: DBDep, db_user: UserDep, start_date: StartDateDep, end_date: EndDateDep +): """ Get users usage """ @@ -284,10 +276,7 @@ def get_user_usage(db: DBDep, @router.put("/{username}/set-owner", response_model=UserResponse) -def set_owner(username: str, - admin_username: str, - db: DBDep, - admin: SudoAdminDep): +def set_owner(username: str, admin_username: str, db: DBDep, admin: SudoAdminDep): db_user = crud.get_user(db, username) if not db_user: raise HTTPException(status_code=404, detail="User not found") diff --git a/app/tasks/__init__.py b/app/tasks/__init__.py index 85bfd27..7d1e3d0 100644 --- a/app/tasks/__init__.py +++ b/app/tasks/__init__.py @@ -14,5 +14,5 @@ __all__ = [ "delete_expired_reminders", "send_notifications", "store_bandwidth", - "record_realtime_bandwidth" + "record_realtime_bandwidth", ] diff --git a/app/tasks/record_usages.py b/app/tasks/record_usages.py index 599a5cf..8320d37 100644 --- a/app/tasks/record_usages.py +++ b/app/tasks/record_usages.py @@ -11,42 +11,55 @@ from app.db.models import NodeUsage, NodeUserUsage, User from app.marznode import MarzNodeBase -def record_user_stats(params: list, node_id: int, - consumption_factor: int = 1): +def record_user_stats(params: list, node_id: int, consumption_factor: int = 1): if not params: return - created_at = datetime.fromisoformat(datetime.utcnow().strftime('%Y-%m-%dT%H:00:00')) + created_at = datetime.fromisoformat(datetime.utcnow().strftime("%Y-%m-%dT%H:00:00")) with GetDB() as db: # make user usage row if doesn't exist - select_stmt = select(NodeUserUsage.user_id) \ - .where(and_(NodeUserUsage.node_id == node_id, NodeUserUsage.created_at == created_at)) + select_stmt = select(NodeUserUsage.user_id).where( + and_( + NodeUserUsage.node_id == node_id, NodeUserUsage.created_at == created_at + ) + ) existings = [r[0] for r in db.execute(select_stmt).fetchall()] uids_to_insert = set() for p in params: - uid = p['uid'] + uid = p["uid"] if uid in existings: continue uids_to_insert.add(uid) if uids_to_insert: stmt = insert(NodeUserUsage).values( - user_id=bindparam('uid'), + user_id=bindparam("uid"), created_at=created_at, node_id=node_id, - used_traffic=0 + used_traffic=0, ) - db.execute(stmt, [{'uid': uid} for uid in uids_to_insert]) + db.execute(stmt, [{"uid": uid} for uid in uids_to_insert]) # record - stmt = update(NodeUserUsage) \ - .values(used_traffic=NodeUserUsage.used_traffic + bindparam('value') * consumption_factor) \ - .where(and_(NodeUserUsage.user_id == bindparam('uid'), - NodeUserUsage.node_id == node_id, - NodeUserUsage.created_at == created_at)) - db.connection().execute(stmt, params, execution_options={"synchronize_session": None}) + stmt = ( + update(NodeUserUsage) + .values( + used_traffic=NodeUserUsage.used_traffic + + bindparam("value") * consumption_factor + ) + .where( + and_( + NodeUserUsage.user_id == bindparam("uid"), + NodeUserUsage.node_id == node_id, + NodeUserUsage.created_at == created_at, + ) + ) + ) + db.connection().execute( + stmt, params, execution_options={"synchronize_session": None} + ) db.commit() @@ -54,23 +67,33 @@ def record_node_stats(params: dict, node_id: int): if not params: return - created_at = datetime.fromisoformat(datetime.utcnow().strftime('%Y-%m-%dT%H:00:00')) + created_at = datetime.fromisoformat(datetime.utcnow().strftime("%Y-%m-%dT%H:00:00")) with GetDB() as db: # make node usage row if doesn't exist - select_stmt = select(NodeUsage.node_id). \ - where(and_(NodeUsage.node_id == node_id, NodeUsage.created_at == created_at)) + select_stmt = select(NodeUsage.node_id).where( + and_(NodeUsage.node_id == node_id, NodeUsage.created_at == created_at) + ) notfound = db.execute(select_stmt).first() is None if notfound: - stmt = insert(NodeUsage).values(created_at=created_at, node_id=node_id, uplink=0, downlink=0) + stmt = insert(NodeUsage).values( + created_at=created_at, node_id=node_id, uplink=0, downlink=0 + ) db.execute(stmt) db.commit() # record - stmt = update(NodeUsage). \ - values(uplink=NodeUsage.uplink + bindparam('up'), downlink=NodeUsage.downlink + bindparam('down')). \ - where(and_(NodeUsage.node_id == node_id, NodeUsage.created_at == created_at)) + stmt = ( + update(NodeUsage) + .values( + uplink=NodeUsage.uplink + bindparam("up"), + downlink=NodeUsage.downlink + bindparam("down"), + ) + .where( + and_(NodeUsage.node_id == node_id, NodeUsage.created_at == created_at) + ) + ) db.execute(stmt, params) @@ -90,32 +113,39 @@ async def record_user_usages(): # usage_coefficient = {None: 1} # default usage coefficient for the main api instance results = await asyncio.gather( - *[get_users_stats(node_id, node) for node_id, node in marznode.nodes.items()] - ) + *[get_users_stats(node_id, node) for node_id, node in marznode.nodes.items()] + ) api_params = {node_id: params for node_id, params in list(results)} users_usage = defaultdict(int) for node_id, params in api_params.items(): - coefficient = node.usage_coefficient if (node := marznode.nodes.get(node_id)) else 1 + coefficient = ( + node.usage_coefficient if (node := marznode.nodes.get(node_id)) else 1 + ) for param in params: - users_usage[param['uid']] += param['value'] * coefficient # apply the usage coefficient + users_usage[param["uid"]] += ( + param["value"] * coefficient + ) # apply the usage coefficient - users_usage = list({"id": uid, "value": value} for uid, value in users_usage.items()) + users_usage = list( + {"id": uid, "value": value} for uid, value in users_usage.items() + ) if not users_usage: return # record users usage with GetDB() as db: - stmt = update(User)\ - .values( - used_traffic=User.used_traffic + bindparam('value'), - online_at=datetime.utcnow() + stmt = update(User).values( + used_traffic=User.used_traffic + bindparam("value"), + online_at=datetime.utcnow(), ) db.execute(stmt, users_usage, execution_options={"synchronize_session": None}) db.commit() for node_id, params in api_params.items(): - record_user_stats(params, node_id, - node.usage_coefficient if (node := marznode.nodes.get(node_id)) else 1) - + record_user_stats( + params, + node_id, + node.usage_coefficient if (node := marznode.nodes.get(node_id)) else 1, + ) diff --git a/app/tasks/reset_user_data_usage.py b/app/tasks/reset_user_data_usage.py index 12c98fc..eb17e7e 100644 --- a/app/tasks/reset_user_data_usage.py +++ b/app/tasks/reset_user_data_usage.py @@ -18,12 +18,15 @@ reset_strategy_to_days = { async def reset_user_data_usage(): now = datetime.utcnow() with GetDB() as db: - for user in get_users(db, reset_strategy=[ - UserDataLimitResetStrategy.day.value, - UserDataLimitResetStrategy.week.value, - UserDataLimitResetStrategy.month.value, - UserDataLimitResetStrategy.year.value, - ]): + for user in get_users( + db, + reset_strategy=[ + UserDataLimitResetStrategy.day.value, + UserDataLimitResetStrategy.week.value, + UserDataLimitResetStrategy.month.value, + UserDataLimitResetStrategy.year.value, + ], + ): last_reset_time = user.last_traffic_reset_time num_days_to_reset = reset_strategy_to_days[user.data_limit_reset_strategy] @@ -36,4 +39,4 @@ async def reset_user_data_usage(): if user.status == UserStatus.active and old_status == UserStatus.limited: await marznode.operations.update_user(user) - logger.info(f"User data usage reset for User \"{user.username}\"") + logger.info(f'User data usage reset for User "{user.username}"') diff --git a/app/tasks/review_users.py b/app/tasks/review_users.py index b02fd92..1399f0e 100644 --- a/app/tasks/review_users.py +++ b/app/tasks/review_users.py @@ -6,14 +6,17 @@ from typing import TYPE_CHECKING from sqlalchemy.orm import Session from app import marznode -from app.db import (GetDB, get_notification_reminder, get_users, - start_user_expire, update_user_status) +from app.db import ( + GetDB, + get_notification_reminder, + get_users, + start_user_expire, + update_user_status, +) from app.models.user import ReminderType, UserResponse, UserStatus from app.utils import report -from app.utils.helpers import (calculate_expiration_days, - calculate_usage_percent) -from config import (NOTIFY_DAYS_LEFT, NOTIFY_REACHED_USAGE_PERCENT, - WEBHOOK_ADDRESS) +from app.utils.helpers import calculate_expiration_days, calculate_usage_percent +from config import NOTIFY_DAYS_LEFT, NOTIFY_REACHED_USAGE_PERCENT, WEBHOOK_ADDRESS if TYPE_CHECKING: from app.db.models import User @@ -22,21 +25,30 @@ if TYPE_CHECKING: logger = logging.getLogger(__name__) -def add_notification_reminders(db: Session, user: "User", now: datetime = datetime.utcnow()) -> None: +def add_notification_reminders( + db: Session, user: "User", now: datetime = datetime.utcnow() +) -> None: if user.data_limit: - usage_percent = calculate_usage_percent( - user.used_traffic, user.data_limit) - if (usage_percent >= NOTIFY_REACHED_USAGE_PERCENT) and (not get_notification_reminder(db, user.id, ReminderType.data_usage)): + usage_percent = calculate_usage_percent(user.used_traffic, user.data_limit) + if (usage_percent >= NOTIFY_REACHED_USAGE_PERCENT) and ( + not get_notification_reminder(db, user.id, ReminderType.data_usage) + ): report.data_usage_percent_reached( - db, usage_percent, UserResponse.model_validate(user), - user.id, user.expire) + db, + usage_percent, + UserResponse.model_validate(user), + user.id, + user.expire, + ) if user.expire and ((now - user.created_at).days >= NOTIFY_DAYS_LEFT): expire_days = calculate_expiration_days(user.expire) - if (expire_days <= NOTIFY_DAYS_LEFT) and (not get_notification_reminder(db, user.id, ReminderType.expiration_date)): + if (expire_days <= NOTIFY_DAYS_LEFT) and ( + not get_notification_reminder(db, user.id, ReminderType.expiration_date) + ): report.expire_days_reached( - db, expire_days, UserResponse.model_validate(user), - user.id, user.expire) + db, expire_days, UserResponse.model_validate(user), user.id, user.expire + ) async def review_users(): @@ -58,12 +70,13 @@ async def review_users(): await marznode.operations.remove_user(user) update_user_status(db, user, status) - asyncio.create_task(report.status_change( - user.username, - user.status, - UserResponse.model_validate(user))) + asyncio.create_task( + report.status_change( + user.username, user.status, UserResponse.model_validate(user) + ) + ) - logger.info(f"User \"{user.username}\" status changed to {status}") + logger.info(f'User "{user.username}" status changed to {status}') for user in get_users(db, status=UserStatus.on_hold): @@ -76,7 +89,9 @@ async def review_users(): if user.online_at and base_time <= datetime.timestamp(user.online_at): status = UserStatus.active - elif user.on_hold_timeout and (datetime.timestamp(user.on_hold_timeout) <= (now.timestamp())): + elif user.on_hold_timeout and ( + datetime.timestamp(user.on_hold_timeout) <= (now.timestamp()) + ): # If the user didn't connect within the timeout period, change status to "Active" status = UserStatus.active @@ -85,10 +100,9 @@ async def review_users(): update_user_status(db, user, status) start_user_expire(db, user) - asyncio.create_task(report.status_change( - user.username, - user.status, - UserResponse.model_validate(user))) + asyncio.create_task( + report.status_change( + user.username, user.status, UserResponse.model_validate(user) + ) + ) logger.info(f"on hold user `{user.username}` has been activated") - - diff --git a/app/tasks/send_notifications.py b/app/tasks/send_notifications.py index f1a75e0..36e04df 100644 --- a/app/tasks/send_notifications.py +++ b/app/tasks/send_notifications.py @@ -31,7 +31,9 @@ async def send(data: List[Dict[Any, Any]]) -> bool: try: logger.debug(f"Sending {len(data)} webhook updates to {config.WEBHOOK_ADDRESS}") - r = await aiohttp.request("post", config.WEBHOOK_ADDRESS, json=data, headers=headers) + r = await aiohttp.request( + "post", config.WEBHOOK_ADDRESS, json=data, headers=headers + ) if r.ok: return True logger.error(r) @@ -46,11 +48,13 @@ async def send_notifications(): notifications_to_send = list() try: - while (notification := queue.popleft()): - if (notification.tries > config.NUMBER_OF_RECURRENT_NOTIFICATIONS): + while notification := queue.popleft(): + if notification.tries > config.NUMBER_OF_RECURRENT_NOTIFICATIONS: continue if notification.send_at > dt.utcnow().timestamp(): - queue.append(notification) # add it to the queue again for the next check + queue.append( + notification + ) # add it to the queue again for the next check continue notifications_to_send.append(notification) except IndexError: # if the queue is empty @@ -64,12 +68,14 @@ async def send_notifications(): continue notification.tries += 1 notification.send_at = ( # schedule notification for n seconds later - dt.utcnow() + td(seconds=config.RECURRENT_NOTIFICATIONS_TIMEOUT)).timestamp() + dt.utcnow() + td(seconds=config.RECURRENT_NOTIFICATIONS_TIMEOUT) + ).timestamp() queue.append(notification) def delete_expired_reminders() -> None: with GetDB() as db: - db.query(NotificationReminder).filter(NotificationReminder.expires_at < dt.utcnow()).delete() + db.query(NotificationReminder).filter( + NotificationReminder.expires_at < dt.utcnow() + ).delete() db.commit() - diff --git a/app/tasks/store_bandwidth.py b/app/tasks/store_bandwidth.py index 9152b35..efaedd4 100644 --- a/app/tasks/store_bandwidth.py +++ b/app/tasks/store_bandwidth.py @@ -5,7 +5,19 @@ from app.utils.system import rt_bw def record_realtime_bandwidth() -> None: io = psutil.net_io_counters() - rt_bw.incoming_bytes, rt_bw.bytes_recv = io.bytes_recv - rt_bw.bytes_recv, io.bytes_recv - rt_bw.outgoing_bytes, rt_bw.bytes_sent = io.bytes_sent - rt_bw.bytes_sent, io.bytes_sent - rt_bw.incoming_packets, rt_bw.packets_recv = io.packets_recv - rt_bw.packets_recv, io.packets_recv - rt_bw.outgoing_packets, rt_bw.packet_sent = io.packets_sent - rt_bw.packet_sent, io.packets_sent \ No newline at end of file + rt_bw.incoming_bytes, rt_bw.bytes_recv = ( + io.bytes_recv - rt_bw.bytes_recv, + io.bytes_recv, + ) + rt_bw.outgoing_bytes, rt_bw.bytes_sent = ( + io.bytes_sent - rt_bw.bytes_sent, + io.bytes_sent, + ) + rt_bw.incoming_packets, rt_bw.packets_recv = ( + io.packets_recv - rt_bw.packets_recv, + io.packets_recv, + ) + rt_bw.outgoing_packets, rt_bw.packet_sent = ( + io.packets_sent - rt_bw.packet_sent, + io.packets_sent, + ) diff --git a/app/telegram/__init__.py b/app/telegram/__init__.py index f95e6d3..5594627 100644 --- a/app/telegram/__init__.py +++ b/app/telegram/__init__.py @@ -16,10 +16,13 @@ async def start_bot(): if bot: handler_dir = dirname(__file__) + "/handlers/" for name in handler_names: - spec = importlib.util.spec_from_file_location(name, f"{handler_dir}{name}.py") + spec = importlib.util.spec_from_file_location( + name, f"{handler_dir}{name}.py" + ) spec.loader.exec_module(importlib.util.module_from_spec(spec)) - from app.telegram import utils # setup custom handlers + from app.telegram import utils # setup custom handlers + utils.setup() await bot.polling() @@ -31,7 +34,7 @@ from .handlers.report import ( # noqa report_user_deletion, report_status_change, report_user_usage_reset, - report_user_subscription_revoked + report_user_subscription_revoked, ) __all__ = [ @@ -42,5 +45,5 @@ __all__ = [ "report_user_deletion", "report_status_change", "report_user_usage_reset", - "report_user_subscription_revoked" + "report_user_subscription_revoked", ] diff --git a/app/telegram/handlers/admin.py b/app/telegram/handlers/admin.py index aa00f23..13e070d 100644 --- a/app/telegram/handlers/admin.py +++ b/app/telegram/handlers/admin.py @@ -14,12 +14,16 @@ from telebot.util import user_link from telebot.asyncio_handler_backends import State, StatesGroup from app.db import GetDB, crud -from app.models.user import (UserCreate, UserModify, UserResponse, UserStatus, - UserStatusModify) +from app.models.user import ( + UserCreate, + UserModify, + UserResponse, + UserStatus, + UserStatusModify, +) from app.models.proxy import ProxyTypes from app.telegram import bot -from app.telegram.utils.custom_filters import (query_equals, - query_startswith) +from app.telegram.utils.custom_filters import query_equals, query_startswith from app.telegram.utils.keyboard import BotKeyboard from app.utils.store import MemoryStorage from app.utils.system import cpu_usage, memory_usage, readable_size @@ -33,9 +37,10 @@ from config import TELEGRAM_LOGGER_CHANNEL_ID, TELEGRAM_DEFAULT_VLESS_FLOW mem_store = MemoryStorage() + class botStates(StatesGroup): start = State() - add_data = State() # statesgroup should contain states + add_data = State() # statesgroup should contain states add_time = State() edit_user_data_limit = State() edit_user_expire = State() @@ -84,7 +89,7 @@ def get_system_info(): active_users=users_active, deactivate_users=total_users - users_active, up_speed=readable_size(realtime_bandwidth().outgoing_bytes), - down_speed=readable_size(realtime_bandwidth().incoming_bytes) + down_speed=readable_size(realtime_bandwidth().incoming_bytes), ) @@ -98,56 +103,62 @@ def schedule_delete_message(chat_id, *message_ids: int) -> None: async def cleanup_messages(chat_id: int) -> None: messages: list[int] = mem_store.get(f"{chat_id}:messages_to_delete", []) for message_id in messages: - try: await bot.delete_message(chat_id, message_id) - except: pass + try: + await bot.delete_message(chat_id, message_id) + except: + pass mem_store.set(f"{chat_id}:messages_to_delete", []) -@bot.message_handler(commands=['start', 'help'], is_admin=True) +@bot.message_handler(commands=["start", "help"], is_admin=True) async def help_command(message: types.Message): await cleanup_messages(message.chat.id) # bot.clear_step_handler_by_chat_id(message.chat.id) await bot.delete_state(message.from_user.id, message.chat.id) - return await bot.reply_to(message, """ + return await bot.reply_to( + message, + """ {user_link} Welcome to Marzban Telegram-Bot Admin Panel. Here you can manage your users and proxies. To get started, use the buttons below. """.format( - user_link=user_link(message.from_user) - ), parse_mode="html", reply_markup=BotKeyboard.main_menu()) + user_link=user_link(message.from_user) + ), + parse_mode="html", + reply_markup=BotKeyboard.main_menu(), + ) -@bot.callback_query_handler(query_equals('system'), is_admin=True) +@bot.callback_query_handler(query_equals("system"), is_admin=True) async def system_command(call: types.CallbackQuery): return await bot.edit_message_text( get_system_info(), call.message.chat.id, call.message.message_id, parse_mode="MarkdownV2", - reply_markup=BotKeyboard.main_menu() + reply_markup=BotKeyboard.main_menu(), ) -@bot.callback_query_handler(query_equals('restart'), is_admin=True) +@bot.callback_query_handler(query_equals("restart"), is_admin=True) async def restart_command(call: types.CallbackQuery): await bot.edit_message_text( - 'âš ī¸ Are you sure? This will restart Xray core.', + "âš ī¸ Are you sure? This will restart Xray core.", call.message.chat.id, call.message.message_id, - reply_markup=BotKeyboard.confirm_action(action='do_restart') + reply_markup=BotKeyboard.confirm_action(action="do_restart"), ) -@bot.callback_query_handler(query_startswith('delete:'), is_admin=True) +@bot.callback_query_handler(query_startswith("delete:"), is_admin=True) async def delete_user_command(call: types.CallbackQuery): - username = call.data.split(':')[1] + username = call.data.split(":")[1] await bot.edit_message_text( - f'âš ī¸ Are you sure? This will delete user `{username}`.', + f"âš ī¸ Are you sure? This will delete user `{username}`.", call.message.chat.id, call.message.message_id, parse_mode="markdown", - reply_markup=BotKeyboard.confirm_action( - action='do_delete', username=username) + reply_markup=BotKeyboard.confirm_action(action="do_delete", username=username), ) @@ -159,8 +170,7 @@ async def suspend_user_command(call: types.CallbackQuery): call.message.chat.id, call.message.message_id, parse_mode="markdown", - reply_markup=BotKeyboard.confirm_action( - action="do_suspend", username=username), + reply_markup=BotKeyboard.confirm_action(action="do_suspend", username=username), ) @@ -173,7 +183,8 @@ async def activate_user_command(call: types.CallbackQuery): call.message.message_id, parse_mode="markdown", reply_markup=BotKeyboard.confirm_action( - action="do_activate", username=username), + action="do_activate", username=username + ), ) @@ -186,11 +197,12 @@ async def reset_usage_user_command(call: types.CallbackQuery): call.message.message_id, parse_mode="markdown", reply_markup=BotKeyboard.confirm_action( - action="do_reset_usage", username=username), + action="do_reset_usage", username=username + ), ) -@bot.callback_query_handler(query_equals('edit_all'), is_admin=True) +@bot.callback_query_handler(query_equals("edit_all"), is_admin=True) async def edit_all_command(call: types.CallbackQuery): with GetDB() as db: total_users = crud.get_users_count(db) @@ -198,48 +210,51 @@ async def edit_all_command(call: types.CallbackQuery): disabled_users = crud.get_users_count(db, UserStatus.disabled) expired_users = crud.get_users_count(db, UserStatus.expired) limited_users = crud.get_users_count(db, UserStatus.limited) - text = f''' + text = f""" đŸ‘Ĩ *Total Users*: `{total_users}` ✅ *Active Users*: `{active_users}` ❌ *Disabled Users*: `{disabled_users}` 🕰 *Expired Users*: `{expired_users}` -đŸĒĢ *Limited Users*: `{limited_users}`''' +đŸĒĢ *Limited Users*: `{limited_users}`""" return await bot.edit_message_text( text, call.message.chat.id, call.message.message_id, parse_mode="markdown", - reply_markup=BotKeyboard.edit_all_menu() + reply_markup=BotKeyboard.edit_all_menu(), ) -@bot.callback_query_handler(query_equals('delete_expired'), is_admin=True) +@bot.callback_query_handler(query_equals("delete_expired"), is_admin=True) async def delete_expired_command(call: types.CallbackQuery): await bot.edit_message_text( f"âš ī¸ Are you sure? This will *DELETE All Expired Users*â€ŧī¸", call.message.chat.id, call.message.message_id, parse_mode="markdown", - reply_markup=BotKeyboard.confirm_action(action="do_delete_expired")) + reply_markup=BotKeyboard.confirm_action(action="do_delete_expired"), + ) -@bot.callback_query_handler(query_equals('delete_limited'), is_admin=True) +@bot.callback_query_handler(query_equals("delete_limited"), is_admin=True) async def delete_limited_command(call: types.CallbackQuery): await bot.edit_message_text( f"âš ī¸ Are you sure? This will *DELETE All Limited Users*â€ŧī¸", call.message.chat.id, call.message.message_id, parse_mode="markdown", - reply_markup=BotKeyboard.confirm_action(action="do_delete_limited")) + reply_markup=BotKeyboard.confirm_action(action="do_delete_limited"), + ) -@bot.callback_query_handler(query_equals('add_data'), is_admin=True) +@bot.callback_query_handler(query_equals("add_data"), is_admin=True) async def add_data_command(call: types.CallbackQuery): msg = await bot.edit_message_text( f"🔋 Enter Data Limit to increase or decrease (GB):", call.message.chat.id, call.message.message_id, - reply_markup=BotKeyboard.inline_cancel_action()) + reply_markup=BotKeyboard.inline_cancel_action(), + ) schedule_delete_message(call.message.chat.id, call.message.id) schedule_delete_message(call.message.chat.id, msg.id) # return bot.register_next_step_handler(call.message, add_data_step) @@ -253,28 +268,32 @@ async def add_data_step(message): if not data_limit: raise ValueError except ValueError: - wait_msg = await bot.send_message(message.chat.id, '❌ Data limit must be a number and not zero.') + wait_msg = await bot.send_message( + message.chat.id, "❌ Data limit must be a number and not zero." + ) schedule_delete_message(message.chat.id, wait_msg.message_id) - return # bot.register_next_step_handler(wait_msg, add_data_step) + return # bot.register_next_step_handler(wait_msg, add_data_step) schedule_delete_message(message.chat.id, message.message_id) msg = await bot.send_message( message.chat.id, f"âš ī¸ Are you sure? this will change Data limit of all users according to " f"{'+' if data_limit > 0 else '-'}{readable_size(abs(data_limit *1024*1024*1024))}", parse_mode="html", - reply_markup=BotKeyboard.confirm_action('do_add_data', data_limit)) + reply_markup=BotKeyboard.confirm_action("do_add_data", data_limit), + ) await bot.delete_state(message.from_user.id, message.chat.id) await cleanup_messages(message.chat.id) schedule_delete_message(message.chat.id, msg.id) -@bot.callback_query_handler(query_equals('add_time'), is_admin=True) +@bot.callback_query_handler(query_equals("add_time"), is_admin=True) async def add_time_command(call: types.CallbackQuery): msg = await bot.edit_message_text( f"📅 Enter Days to increase or decrease expiry:", call.message.chat.id, call.message.message_id, - reply_markup=BotKeyboard.inline_cancel_action()) + reply_markup=BotKeyboard.inline_cancel_action(), + ) schedule_delete_message(call.message.chat.id, call.message.id) schedule_delete_message(call.message.chat.id, msg.id) # return bot.register_next_step_handler(call.message, add_time_step) @@ -288,15 +307,18 @@ async def add_time_step(message): if not days: raise ValueError except ValueError: - wait_msg = await bot.send_message(message.chat.id, '❌ Days must be as a number and not zero.') + wait_msg = await bot.send_message( + message.chat.id, "❌ Days must be as a number and not zero." + ) schedule_delete_message(message.chat.id, wait_msg.message_id) - return # bot.register_next_step_handler(wait_msg, add_time_step) + return # bot.register_next_step_handler(wait_msg, add_time_step) schedule_delete_message(message.chat.id, message.message_id) msg = await bot.send_message( message.chat.id, f"âš ī¸ Are you sure? this will change Expiry Time of all users according to {days} Days", parse_mode="html", - reply_markup=BotKeyboard.confirm_action('do_add_time', days)) + reply_markup=BotKeyboard.confirm_action("do_add_time", days), + ) await cleanup_messages(message.chat.id) schedule_delete_message(message.chat.id, msg.id) @@ -308,7 +330,8 @@ async def inbound_command(call: types.CallbackQuery): call.message.chat.id, call.message.message_id, parse_mode="markdown", - reply_markup=BotKeyboard.inbounds_menu(call.data, xray.config.inbounds_by_tag)) + reply_markup=BotKeyboard.inbounds_menu(call.data, xray.config.inbounds_by_tag), + ) @bot.callback_query_handler(query_startswith("confirm_inbound"), is_admin=True) @@ -318,7 +341,8 @@ async def delete_expired_confirm_command(call: types.CallbackQuery): call.message.chat.id, call.message.message_id, parse_mode="markdown", - reply_markup=BotKeyboard.confirm_action(action=call.data[8:])) + reply_markup=BotKeyboard.confirm_action(action=call.data[8:]), + ) @bot.callback_query_handler(query_startswith("edit:"), is_admin=True) @@ -330,16 +354,17 @@ async def edit_command(call: types.CallbackQuery): db_user = crud.get_user(db, username) if not db_user: return await bot.answer_callback_query( - call.id, - '❌ User not found.', - show_alert=True + call.id, "❌ User not found.", show_alert=True ) services = crud.get_services(db) user = UserResponse.model_validate(db_user) - mem_store.set(f'{call.message.chat.id}:username', username) - mem_store.set(f'{call.message.chat.id}:data_limit', db_user.data_limit) - mem_store.set(f'{call.message.chat.id}:expire_date', datetime.fromtimestamp(db_user.expire) if db_user.expire else None) - mem_store.set(f'{call.message.chat.id}:services', [s.id for s in db_user.services]) + mem_store.set(f"{call.message.chat.id}:username", username) + mem_store.set(f"{call.message.chat.id}:data_limit", db_user.data_limit) + mem_store.set( + f"{call.message.chat.id}:expire_date", + datetime.fromtimestamp(db_user.expire) if db_user.expire else None, + ) + mem_store.set(f"{call.message.chat.id}:services", [s.id for s in db_user.services]) await bot.edit_message_text( f"📝 Editing user `{username}`", call.message.chat.id, @@ -352,20 +377,18 @@ async def edit_command(call: types.CallbackQuery): username=username, data_limit=db_user.data_limit, expire_date=mem_store.get(f"{call.message.chat.id}:expire_date"), - ) + ), ) -@bot.callback_query_handler(query_equals('help_edit'), is_admin=True) +@bot.callback_query_handler(query_equals("help_edit"), is_admin=True) async def help_edit_command(call: types.CallbackQuery): await bot.answer_callback_query( - call.id, - text="Press the (âœī¸ Edit) button to edit", - show_alert=True + call.id, text="Press the (âœī¸ Edit) button to edit", show_alert=True ) -@bot.callback_query_handler(query_equals('cancel'), is_admin=True) +@bot.callback_query_handler(query_equals("cancel"), is_admin=True) async def cancel_command(call: types.CallbackQuery): # bot.clear_step_handler_by_chat_id(call.message.chat.id) await bot.delete_state(call.from_user.id, call.message.chat.id) @@ -374,11 +397,11 @@ async def cancel_command(call: types.CallbackQuery): call.message.chat.id, call.message.message_id, parse_mode="MarkdownV2", - reply_markup=BotKeyboard.main_menu() + reply_markup=BotKeyboard.main_menu(), ) -@bot.callback_query_handler(query_startswith('edit_user:'), is_admin=True) +@bot.callback_query_handler(query_startswith("edit_user:"), is_admin=True) async def edit_user_command(call: types.CallbackQuery): _, username, action = call.data.split(":") schedule_delete_message(call.message.chat.id, call.message.id) @@ -386,29 +409,34 @@ async def edit_user_command(call: types.CallbackQuery): if action == "data": msg = await bot.send_message( call.message.chat.id, - 'âŦ†ī¸ Enter Data Limit (GB):\nâš ī¸ Send 0 for unlimited.', - reply_markup=BotKeyboard.inline_cancel_action(f'user:{username}') + "âŦ†ī¸ Enter Data Limit (GB):\nâš ī¸ Send 0 for unlimited.", + reply_markup=BotKeyboard.inline_cancel_action(f"user:{username}"), ) mem_store.set(f"{call.message.chat.id}:edit_msg_text", call.message.text) await bot.delete_state(call.from_user.id, call.message.chat.id) - await bot.set_state(call.from_user.id, botStates.edit_user_data_limit, call.message.chat.id) + await bot.set_state( + call.from_user.id, botStates.edit_user_data_limit, call.message.chat.id + ) async with bot.retrieve_data(call.from_user.id, call.message.chat.id) as data: - data["username"] = username - #bot.clear_step_handler_by_chat_id(call.message.chat.id) - #bot.register_next_step_handler( + data["username"] = username + # bot.clear_step_handler_by_chat_id(call.message.chat.id) + # bot.register_next_step_handler( # call.message, edit_user_data_limit_step, username) schedule_delete_message(call.message.chat.id, msg.message_id) elif action == "expire": msg = await bot.send_message( call.message.chat.id, - 'âŦ†ī¸ Enter Expire Date (YYYY-MM-DD)\nOr You Can Use Regex Symbol: ^[0-9]{1,3}(M|D) :\nâš ī¸ Send 0 for never expire.', - reply_markup=BotKeyboard.inline_cancel_action(f'user:{username}')) + "âŦ†ī¸ Enter Expire Date (YYYY-MM-DD)\nOr You Can Use Regex Symbol: ^[0-9]{1,3}(M|D) :\nâš ī¸ Send 0 for never expire.", + reply_markup=BotKeyboard.inline_cancel_action(f"user:{username}"), + ) mem_store.set(f"{call.message.chat.id}:edit_msg_text", call.message.text) # bot.clear_step_handler_by_chat_id(call.message.chat.id) - await bot.set_state(call.from_user.id, botStates.edit_user_expire, call.message.chat.id) + await bot.set_state( + call.from_user.id, botStates.edit_user_expire, call.message.chat.id + ) async with bot.retrieve_data(call.from_user.id, call.message.chat.id) as data: - data["username"] = username - #bot.register_next_step_handler( + data["username"] = username + # bot.register_next_step_handler( # call.message, edit_user_expire_step, username=username) schedule_delete_message(call.message.chat.id, msg.message_id) @@ -417,15 +445,19 @@ async def edit_user_command(call: types.CallbackQuery): async def edit_user_data_limit_step(message: types.Message): try: if float(message.text) < 0: - wait_msg = await bot.send_message(message.chat.id, '❌ Data limit must be greater or equal to 0.') + wait_msg = await bot.send_message( + message.chat.id, "❌ Data limit must be greater or equal to 0." + ) schedule_delete_message(message.chat.id, wait_msg.message_id) - return # bot.register_next_step_handler(wait_msg, edit_user_data_limit_step, username=username) + return # bot.register_next_step_handler(wait_msg, edit_user_data_limit_step, username=username) data_limit = float(message.text) * 1024**3 except ValueError: - wait_msg = await bot.send_message(message.chat.id, '❌ Data limit must be a number.') + wait_msg = await bot.send_message( + message.chat.id, "❌ Data limit must be a number." + ) schedule_delete_message(message.chat.id, wait_msg.message_id) - return # bot.register_next_step_handler(wait_msg, edit_user_data_limit_step, username=username) - mem_store.set(f'{message.chat.id}:data_limit', data_limit) + return # bot.register_next_step_handler(wait_msg, edit_user_data_limit_step, username=username) + mem_store.set(f"{message.chat.id}:data_limit", data_limit) schedule_delete_message(message.chat.id, message.message_id) text = mem_store.get(f"{message.chat.id}:edit_msg_text") mem_store.delete(f"{message.chat.id}:edit_msg_text") @@ -437,8 +469,13 @@ async def edit_user_data_limit_step(message: types.Message): text or f"📝 Editing user {username}", parse_mode="html", reply_markup=BotKeyboard.select_protocols( - mem_store.get(f'{message.chat.id}:protocols'), "edit", - username=username, data_limit=data_limit, expire_date=mem_store.get(f'{message.chat.id}:expire_date'))) + mem_store.get(f"{message.chat.id}:protocols"), + "edit", + username=username, + data_limit=data_limit, + expire_date=mem_store.get(f"{message.chat.id}:expire_date"), + ), + ) await cleanup_messages(message.chat.id) @@ -447,37 +484,37 @@ async def edit_user_expire_step(message: types.Message): try: now = datetime.now() today = datetime( - year=now.year, - month=now.month, - day=now.day, - hour=23, - minute=59, - second=59 + year=now.year, month=now.month, day=now.day, hour=23, minute=59, second=59 ) - if re.match(r'^[0-9]{1,3}(M|m|D|d)$', message.text): + if re.match(r"^[0-9]{1,3}(M|m|D|d)$", message.text): expire_date = today - number_pattern = r'^[0-9]{1,3}' + number_pattern = r"^[0-9]{1,3}" number = int(re.findall(number_pattern, message.text)[0]) - symbol_pattern = r'(M|m|D|d)$' + symbol_pattern = r"(M|m|D|d)$" symbol = re.findall(symbol_pattern, message.text)[0].upper() - if symbol == 'M': + if symbol == "M": expire_date = today + relativedelta(months=number) - elif symbol == 'D': + elif symbol == "D": expire_date = today + relativedelta(days=number) - elif message.text != '0': + elif message.text != "0": expire_date = datetime.strptime(message.text, "%Y-%m-%d") else: expire_date = None if expire_date and expire_date < today: - wait_msg = await bot.send_message(message.chat.id, '❌ Expire date must be greater than today.') + wait_msg = await bot.send_message( + message.chat.id, "❌ Expire date must be greater than today." + ) schedule_delete_message(message.chat.id, wait_msg.message_id) - return # bot.register_next_step_handler(wait_msg, edit_user_expire_step, username=username) + return # bot.register_next_step_handler(wait_msg, edit_user_expire_step, username=username) except ValueError: - wait_msg = await bot.send_message(message.chat.id, '❌ Expire date must be in YYYY-MM-DD format.\nOr You Can Use Regex Symbol: ^[0-9]{1,3}(M|D)') + wait_msg = await bot.send_message( + message.chat.id, + "❌ Expire date must be in YYYY-MM-DD format.\nOr You Can Use Regex Symbol: ^[0-9]{1,3}(M|D)", + ) schedule_delete_message(message.chat.id, wait_msg.message_id) - return # bot.register_next_step_handler(wait_msg, edit_user_expire_step, username=username) + return # bot.register_next_step_handler(wait_msg, edit_user_expire_step, username=username) - mem_store.set(f'{message.chat.id}:expire_date', expire_date) + mem_store.set(f"{message.chat.id}:expire_date", expire_date) schedule_delete_message(message.chat.id, message.message_id) text = mem_store.get(f"{message.chat.id}:edit_msg_text") mem_store.delete(f"{message.chat.id}:edit_msg_text") @@ -489,41 +526,54 @@ async def edit_user_expire_step(message: types.Message): text or f"📝 Editing user {username}", parse_mode="html", reply_markup=BotKeyboard.select_protocols( - mem_store.get(f'{message.chat.id}:protocols'), "edit", - username=username, data_limit=mem_store.get(f'{message.chat.id}:data_limit'), expire_date=expire_date)) + mem_store.get(f"{message.chat.id}:protocols"), + "edit", + username=username, + data_limit=mem_store.get(f"{message.chat.id}:data_limit"), + expire_date=expire_date, + ), + ) await cleanup_messages(message.chat.id) -@bot.callback_query_handler(query_startswith('users:'), is_admin=True) +@bot.callback_query_handler(query_startswith("users:"), is_admin=True) async def users_command(call: types.CallbackQuery): - page = int(call.data.split(':')[1]) if len(call.data.split(':')) > 1 else 1 + page = int(call.data.split(":")[1]) if len(call.data.split(":")) > 1 else 1 with GetDB() as db: total_pages = math.ceil(crud.get_users_count(db) / 10) - users = crud.get_users(db, offset=(page - 1) * 10, limit=10, sort=[crud.UsersSortingOptions["-created_at"]]) + users = crud.get_users( + db, + offset=(page - 1) * 10, + limit=10, + sort=[crud.UsersSortingOptions["-created_at"]], + ) text = """đŸ‘Ĩ Users: (Page {page}/{total_pages}) ✅ Active ❌ Disabled 🕰 Expired -đŸĒĢ Limited""".format(page=page, total_pages=total_pages) +đŸĒĢ Limited""".format( + page=page, total_pages=total_pages + ) await bot.edit_message_text( text, call.message.chat.id, call.message.message_id, parse_mode="HTML", - reply_markup=BotKeyboard.user_list( - users, page, total_pages=total_pages) + reply_markup=BotKeyboard.user_list(users, page, total_pages=total_pages), ) def get_user_info_text( - status: str, username: str, sub_url: str, data_limit: int = None, - usage: int = None, expire: int = None, note: str = None) -> str: - statuses = { - 'active': '✅', - 'expired': '🕰', - 'limited': 'đŸĒĢ', - 'disabled': '❌'} + status: str, + username: str, + sub_url: str, + data_limit: int = None, + usage: int = None, + expire: int = None, + note: str = None, +) -> str: + statuses = {"active": "✅", "expired": "🕰", "limited": "đŸĒĢ", "disabled": "❌"} text = f"""\ ┌─{statuses[status]} Status: {status.title()} │ └─Username: {username} @@ -536,47 +586,54 @@ def get_user_info_text( │ """ if note: - text += f'├─📝 Note: {note}\n│\n' + text += f"├─📝 Note: {note}\n│\n" text += f'└─🚀 Subscription: {sub_url}' return text - -@bot.callback_query_handler(query_startswith('edit_note:'), is_admin=True) +@bot.callback_query_handler(query_startswith("edit_note:"), is_admin=True) async def edit_note_command(call: types.CallbackQuery): - username = call.data.split(':')[1] + username = call.data.split(":")[1] with GetDB() as db: db_user = crud.get_user(db, username) if not db_user: - return await bot.answer_callback_query(call.id, '❌ User not found.', show_alert=True) + return await bot.answer_callback_query( + call.id, "❌ User not found.", show_alert=True + ) schedule_delete_message(call.message.chat.id, call.message.id) await cleanup_messages(call.message.chat.id) msg = await bot.send_message( call.message.chat.id, - f'📝 Current Note: {db_user.note}\n\nSend new Note for {username}', + f"📝 Current Note: {db_user.note}\n\nSend new Note for {username}", parse_mode="HTML", - reply_markup=BotKeyboard.inline_cancel_action(f'user:{username}')) - mem_store.set(f'{call.message.chat.id}:username', username) + reply_markup=BotKeyboard.inline_cancel_action(f"user:{username}"), + ) + mem_store.set(f"{call.message.chat.id}:username", username) schedule_delete_message(call.message.chat.id, msg.id) await bot.set_state(call.from_user.id, botStates.edit_note, call.message.chat.id) # bot.register_next_step_handler(msg, edit_note_step) + @bot.message_handler(state=botStates.edit_note) async def edit_note_step(message: types.Message): - note = message.text or '' + note = message.text or "" if len(note) > 500: - wait_msg = await bot.send_message(message.chat.id, '❌ Note can not be more than 500 characters.') + wait_msg = await bot.send_message( + message.chat.id, "❌ Note can not be more than 500 characters." + ) schedule_delete_message(message.chat.id, wait_msg.id) schedule_delete_message(message.chat.id, message.id) - return # bot.register_next_step_handler(wait_msg, edit_note_step) - username = mem_store.get(f'{message.chat.id}:username') + return # bot.register_next_step_handler(wait_msg, edit_note_step) + username = mem_store.get(f"{message.chat.id}:username") if not username: await cleanup_messages(message.chat.id) - await bot.reply_to(message, '❌ Something went wrong!\n restart bot /start') + await bot.reply_to(message, "❌ Something went wrong!\n restart bot /start") with GetDB() as db: db_user = crud.get_user(db, username) last_note = db_user.note - db_user = crud.update_user(db, db_user, UserModify(username=username, note=note)) + db_user = crud.update_user( + db, db_user, UserModify(username=username, note=note) + ) user = UserResponse.model_validate(db_user) text = get_user_info_text( status=user.status, @@ -585,50 +642,62 @@ async def edit_note_step(message: types.Message): expire=user.expire, data_limit=user.data_limit, usage=user.used_traffic, - note=user.note) - await bot.reply_to(message, text, parse_mode="html", reply_markup=BotKeyboard.user_menu(user_info={ - 'status': user.status, - 'username': user.username}, note=True)) + note=user.note, + ) + await bot.reply_to( + message, + text, + parse_mode="html", + reply_markup=BotKeyboard.user_menu( + user_info={"status": user.status, "username": user.username}, note=True + ), + ) if TELEGRAM_LOGGER_CHANNEL_ID: - text = f'''\ + text = f"""\ 📝 #Edit_Note #From_Bot ➖➖➖➖➖➖➖➖➖ Username : {username} Last Note : {last_note} New Note : {note} ➖➖➖➖➖➖➖➖➖ -By : {message.from_user.full_name}''' +By : {message.from_user.full_name}""" try: - await bot.send_message(TELEGRAM_LOGGER_CHANNEL_ID, text, 'HTML') + await bot.send_message(TELEGRAM_LOGGER_CHANNEL_ID, text, "HTML") except: pass - -@bot.callback_query_handler(query_startswith('user:'), is_admin=True) +@bot.callback_query_handler(query_startswith("user:"), is_admin=True) async def user_command(call: types.CallbackQuery): await bot.delete_state(call.from_user.id, call.message.chat.id) # bot.clear_step_handler_by_chat_id(call.message.chat.id) - username = call.data.split(':')[1] - page = int(call.data.split(':')[2]) if len(call.data.split(':')) > 2 else 1 + username = call.data.split(":")[1] + page = int(call.data.split(":")[2]) if len(call.data.split(":")) > 2 else 1 with GetDB() as db: db_user = crud.get_user(db, username) if not db_user: return await bot.answer_callback_query( - call.id, - '❌ User not found.', - show_alert=True + call.id, "❌ User not found.", show_alert=True ) user = UserResponse.model_validate(db_user) text = get_user_info_text( - status=user.status, username=username, sub_url=user.subscription_url, - data_limit=user.data_limit, usage=user.used_traffic, expire=user.expire, note=user.note) + status=user.status, + username=username, + sub_url=user.subscription_url, + data_limit=user.data_limit, + usage=user.used_traffic, + expire=user.expire, + note=user.note, + ) await bot.edit_message_text( text, - call.message.chat.id, call.message.message_id, parse_mode="HTML", + call.message.chat.id, + call.message.message_id, + parse_mode="HTML", reply_markup=BotKeyboard.user_menu( - {'username': user.username, 'status': user.status}, - page=page, note=True)) + {"username": user.username, "status": user.status}, page=page, note=True + ), + ) @bot.callback_query_handler(query_startswith("revoke_sub:"), is_admin=True) @@ -639,7 +708,8 @@ async def revoke_sub_command(call: types.CallbackQuery): call.message.chat.id, call.message.message_id, parse_mode="markdown", - reply_markup=BotKeyboard.confirm_action(action="do_revoke", username=username)) + reply_markup=BotKeyboard.confirm_action(action="do_revoke", username=username), + ) @bot.callback_query_handler(query_startswith("links:"), is_admin=True) @@ -649,7 +719,9 @@ async def links_command(call: types.CallbackQuery): with GetDB() as db: db_user = crud.get_user(db, username) if not db_user: - return await bot.answer_callback_query(call.id, "User not found!", show_alert=True) + return await bot.answer_callback_query( + call.id, "User not found!", show_alert=True + ) user = UserResponse.model_validate(db_user) @@ -662,7 +734,7 @@ async def links_command(call: types.CallbackQuery): call.message.chat.id, call.message.message_id, parse_mode="HTML", - reply_markup=BotKeyboard.show_links(username) + reply_markup=BotKeyboard.show_links(username), ) @@ -673,7 +745,9 @@ async def genqr_command(call: types.CallbackQuery): with GetDB() as db: db_user = crud.get_user(db, username) if not db_user: - return await bot.answer_callback_query(call.id, "User not found!", show_alert=True) + return await bot.answer_callback_query( + call.id, "User not found!", show_alert=True + ) user = UserResponse.model_validate(db_user) @@ -689,7 +763,7 @@ async def genqr_command(call: types.CallbackQuery): call.message.chat.id, photo=f, caption=f"{link}", - parse_mode="HTML" + parse_mode="HTML", ) with io.BytesIO() as f: qr = qrcode.QRCode(border=6) @@ -705,10 +779,10 @@ async def genqr_command(call: types.CallbackQuery): sub_url=user.subscription_url, data_limit=user.data_limit, usage=user.used_traffic, - expire=user.expire + expire=user.expire, ), parse_mode="HTML", - reply_markup=BotKeyboard.subscription_page(user.subscription_url) + reply_markup=BotKeyboard.subscription_page(user.subscription_url), ) try: await bot.delete_message(call.message.chat.id, call.message.message_id) @@ -723,33 +797,43 @@ async def genqr_command(call: types.CallbackQuery): call.message.chat.id, text, "HTML", - reply_markup=BotKeyboard.show_links(username) + reply_markup=BotKeyboard.show_links(username), ) -@bot.callback_query_handler(query_startswith('random'), is_admin=True) + +@bot.callback_query_handler(query_startswith("random"), is_admin=True) async def random_username(call: types.CallbackQuery): # bot.clear_step_handler_by_chat_id(call.message.chat.id) await bot.delete_state(call.from_user.id, call.message.chat.id) template_id = int(call.data.split(":")[1] or 0) - mem_store.delete(f'{call.message.chat.id}:template_id') + mem_store.delete(f"{call.message.chat.id}:template_id") characters = string.ascii_letters + string.digits - username = random.choice(characters) + ''.join(random.choices(characters, k=4)) + '_' + ''.join(random.choices(characters, k=4)) + username = ( + random.choice(characters) + + "".join(random.choices(characters, k=4)) + + "_" + + "".join(random.choices(characters, k=4)) + ) schedule_delete_message(call.message.chat.id, call.message.id) await cleanup_messages(call.message.chat.id) - msg = await bot.send_message(call.message.chat.id, - 'âŦ†ī¸ Enter Data Limit (GB):\nâš ī¸ Send 0 for unlimited.', - reply_markup=BotKeyboard.inline_cancel_action()) + msg = await bot.send_message( + call.message.chat.id, + "âŦ†ī¸ Enter Data Limit (GB):\nâš ī¸ Send 0 for unlimited.", + reply_markup=BotKeyboard.inline_cancel_action(), + ) schedule_delete_message(call.message.chat.id, msg.id) - await bot.set_state(call.from_user.id, botStates.add_user_data_limit, call.message.chat.id) + await bot.set_state( + call.from_user.id, botStates.add_user_data_limit, call.message.chat.id + ) async with bot.retrieve_data(call.from_user.id, call.message.chat.id) as data: data["username"] = username - return + return -@bot.callback_query_handler(query_equals('add_user'), is_admin=True) +@bot.callback_query_handler(query_equals("add_user"), is_admin=True) async def add_user_command(call: types.CallbackQuery): try: await bot.delete_message(call.message.chat.id, call.message.message_id) @@ -757,42 +841,56 @@ async def add_user_command(call: types.CallbackQuery): pass username_msg = await bot.send_message( call.message.chat.id, - '👤 Enter username:\nâš ī¸Username only can be 3 to 32 characters and contain a-z, A-Z 0-9, and underscores in ' - 'between.', - reply_markup=BotKeyboard.random_username()) + "👤 Enter username:\nâš ī¸Username only can be 3 to 32 characters and contain a-z, A-Z 0-9, and underscores in " + "between.", + reply_markup=BotKeyboard.random_username(), + ) schedule_delete_message(call.message.chat.id, username_msg.id) - await bot.set_state(call.from_user.id, botStates.add_user_username, call.message.chat.id) + await bot.set_state( + call.from_user.id, botStates.add_user_username, call.message.chat.id + ) # bot.register_next_step_handler(username_msg, add_user_username_step) + @bot.message_handler(state=botStates.add_user_username) async def add_user_username_step(message: types.Message): username = message.text if not username: - wait_msg = await bot.send_message(message.chat.id, '❌ Username can not be empty.') + wait_msg = await bot.send_message( + message.chat.id, "❌ Username can not be empty." + ) schedule_delete_message(message.chat.id, wait_msg.id) schedule_delete_message(message.chat.id, message.id) - return # bot.register_next_step_handler(wait_msg, add_user_username_step) - if not re.match(r'^(?!.*__)(?!.*_$)\w{2,31}[a-zA-Z\d]$', username): - wait_msg = await bot.send_message(message.chat.id, - '❌ Username only can be 3 to 32 characters and contain a-z, A-Z, 0-9, and underscores in between.') + return # bot.register_next_step_handler(wait_msg, add_user_username_step) + if not re.match(r"^(?!.*__)(?!.*_$)\w{2,31}[a-zA-Z\d]$", username): + wait_msg = await bot.send_message( + message.chat.id, + "❌ Username only can be 3 to 32 characters and contain a-z, A-Z, 0-9, and underscores in between.", + ) schedule_delete_message(message.chat.id, wait_msg.id) schedule_delete_message(message.chat.id, message.id) - return # bot.register_next_step_handler(wait_msg, add_user_username_step) + return # bot.register_next_step_handler(wait_msg, add_user_username_step) with GetDB() as db: if crud.get_user(db, username): - wait_msg = await bot.send_message(message.chat.id, '❌ Username already exists.') + wait_msg = await bot.send_message( + message.chat.id, "❌ Username already exists." + ) schedule_delete_message(message.chat.id, wait_msg.id) schedule_delete_message(message.chat.id, message.id) - return # bot.register_next_step_handler(wait_msg, add_user_username_step) + return # bot.register_next_step_handler(wait_msg, add_user_username_step) schedule_delete_message(message.chat.id, message.id) await cleanup_messages(message.chat.id) - msg = await bot.send_message(message.chat.id, - 'âŦ†ī¸ Enter Data Limit (GB):\nâš ī¸ Send 0 for unlimited.', - reply_markup=BotKeyboard.inline_cancel_action()) + msg = await bot.send_message( + message.chat.id, + "âŦ†ī¸ Enter Data Limit (GB):\nâš ī¸ Send 0 for unlimited.", + reply_markup=BotKeyboard.inline_cancel_action(), + ) schedule_delete_message(message.chat.id, msg.id) - await bot.set_state(message.from_user.id, botStates.add_user_data_limit, message.chat.id) + await bot.set_state( + message.from_user.id, botStates.add_user_data_limit, message.chat.id + ) async with bot.retrieve_data(message.from_user.id, message.chat.id) as data: - data["username"] = username + data["username"] = username # bot.register_next_step_handler(msg, add_user_data_limit_step, username=username) @@ -800,70 +898,78 @@ async def add_user_username_step(message: types.Message): async def add_user_data_limit_step(message: types.Message): try: if float(message.text) < 0: - wait_msg = await bot.send_message(message.chat.id, '❌ Data limit must be greater or equal to 0.') + wait_msg = await bot.send_message( + message.chat.id, "❌ Data limit must be greater or equal to 0." + ) schedule_delete_message(message.chat.id, wait_msg.id) schedule_delete_message(message.chat.id, message.id) - return #bot.register_next_step_handler(wait_msg, add_user_data_limit_step, username=username) + return # bot.register_next_step_handler(wait_msg, add_user_data_limit_step, username=username) data_limit = float(message.text) * 1024 * 1024 * 1024 except ValueError: - wait_msg = await bot.send_message(message.chat.id, '❌ Data limit must be a number.') + wait_msg = await bot.send_message( + message.chat.id, "❌ Data limit must be a number." + ) schedule_delete_message(message.chat.id, wait_msg.id) schedule_delete_message(message.chat.id, message.id) - return # bot.register_next_step_handler(wait_msg, add_user_data_limit_step, username=username) + return # bot.register_next_step_handler(wait_msg, add_user_data_limit_step, username=username) schedule_delete_message(message.chat.id, message.id) await cleanup_messages(message.chat.id) - msg = await bot.send_message(message.chat.id, - 'âŦ†ī¸ Enter Expire Date (YYYY-MM-DD)\nOr You Can Use Regex Symbol: ^[0-9]{1,3}(M|D) :\nâš ī¸ Send 0 for never expire.', - reply_markup=BotKeyboard.inline_cancel_action()) + msg = await bot.send_message( + message.chat.id, + "âŦ†ī¸ Enter Expire Date (YYYY-MM-DD)\nOr You Can Use Regex Symbol: ^[0-9]{1,3}(M|D) :\nâš ī¸ Send 0 for never expire.", + reply_markup=BotKeyboard.inline_cancel_action(), + ) schedule_delete_message(message.chat.id, msg.id) - await bot.set_state(message.from_user.id, botStates.add_user_expire, message.chat.id) + await bot.set_state( + message.from_user.id, botStates.add_user_expire, message.chat.id + ) async with bot.retrieve_data(message.from_user.id, message.chat.id) as data: data["data_limit"] = data_limit - + # bot.register_next_step_handler(msg, add_user_expire_step, username=username, data_limit=data_limit) + @bot.message_handler(state=botStates.add_user_expire) async def add_user_expire_step(message: types.Message): try: now = datetime.now() today = datetime( - year=now.year, - month=now.month, - day=now.day, - hour=23, - minute=59, - second=59 + year=now.year, month=now.month, day=now.day, hour=23, minute=59, second=59 ) - if re.match(r'^[0-9]{1,3}(M|m|D|d)$', message.text): + if re.match(r"^[0-9]{1,3}(M|m|D|d)$", message.text): expire_date = today - number_pattern = r'^[0-9]{1,3}' + number_pattern = r"^[0-9]{1,3}" number = int(re.findall(number_pattern, message.text)[0]) - symbol_pattern = r'(M|m|D|d)$' + symbol_pattern = r"(M|m|D|d)$" symbol = re.findall(symbol_pattern, message.text)[0].upper() - if symbol == 'M': + if symbol == "M": expire_date = today + relativedelta(months=number) - elif symbol == 'D': + elif symbol == "D": expire_date = today + relativedelta(days=number) - elif message.text != '0': + elif message.text != "0": expire_date = datetime.strptime(message.text, "%Y-%m-%d") else: expire_date = None if expire_date and expire_date < today: - wait_msg = await bot.send_message(message.chat.id, '❌ Expire date must be greater than today.') + wait_msg = await bot.send_message( + message.chat.id, "❌ Expire date must be greater than today." + ) schedule_delete_message(message.chat.id, wait_msg.id) schedule_delete_message(message.chat.id, message.id) - return # bot.register_next_step_handler(wait_msg, add_user_expire_step, username=username, data_limit=data_limit) + return # bot.register_next_step_handler(wait_msg, add_user_expire_step, username=username, data_limit=data_limit) except ValueError: - wait_msg = await bot.send_message(message.chat.id, - '❌ Expire date must be in YYYY-MM-DD format.\nOr You Can Use Regex Symbol: ^[0-9]{1,3}(M|D)') + wait_msg = await bot.send_message( + message.chat.id, + "❌ Expire date must be in YYYY-MM-DD format.\nOr You Can Use Regex Symbol: ^[0-9]{1,3}(M|D)", + ) schedule_delete_message(message.chat.id, wait_msg.id) schedule_delete_message(message.chat.id, message.id) - return # bot.register_next_step_handler(wait_msg, add_user_expire_step, username=username, data_limit=data_limit) + return # bot.register_next_step_handler(wait_msg, add_user_expire_step, username=username, data_limit=data_limit) async with bot.retrieve_data(message.from_user.id, message.chat.id) as data: username, data_limit = data["username"], data["data_limit"] - mem_store.set(f'{message.chat.id}:username', username) - mem_store.set(f'{message.chat.id}:data_limit', data_limit) - mem_store.set(f'{message.chat.id}:expire_date', expire_date) + mem_store.set(f"{message.chat.id}:username", username) + mem_store.set(f"{message.chat.id}:data_limit", data_limit) + mem_store.set(f"{message.chat.id}:expire_date", expire_date) with GetDB() as db: services = crud.get_services(db) @@ -871,23 +977,31 @@ async def add_user_expire_step(message: types.Message): await cleanup_messages(message.chat.id) await bot.send_message( message.chat.id, - 'Select Services:\nUsernames: {}\nData Limit: {}\nExpiry Date {}'.format( - mem_store.get(f'{message.chat.id}:username'), - readable_size(mem_store.get(f'{message.chat.id}:data_limit')) - if mem_store.get(f'{message.chat.id}:data_limit') else "Unlimited", - mem_store.get(f'{message.chat.id}:expire_date').strftime("%Y-%m-%d") - if mem_store.get(f'{message.chat.id}:expire_date') else 'Never' + "Select Services:\nUsernames: {}\nData Limit: {}\nExpiry Date {}".format( + mem_store.get(f"{message.chat.id}:username"), + ( + readable_size(mem_store.get(f"{message.chat.id}:data_limit")) + if mem_store.get(f"{message.chat.id}:data_limit") + else "Unlimited" + ), + ( + mem_store.get(f"{message.chat.id}:expire_date").strftime("%Y-%m-%d") + if mem_store.get(f"{message.chat.id}:expire_date") + else "Never" + ), ), - reply_markup=BotKeyboard.select_services(services, [], action="create") + reply_markup=BotKeyboard.select_services(services, [], action="create"), ) -@bot.callback_query_handler(query_startswith('switch_service:'), is_admin=True) +@bot.callback_query_handler(query_startswith("switch_service:"), is_admin=True) async def switch_service(call: types.CallbackQuery): - if not (username := mem_store.get(f'{call.message.chat.id}:username')): - return await bot.answer_callback_query(call.id, '❌ No user selected.', show_alert=True) - selected_services: List[int] = mem_store.get(f'{call.message.chat.id}:services', []) - _, service, action = call.data.split(':') + if not (username := mem_store.get(f"{call.message.chat.id}:username")): + return await bot.answer_callback_query( + call.id, "❌ No user selected.", show_alert=True + ) + selected_services: List[int] = mem_store.get(f"{call.message.chat.id}:services", []) + _, service, action = call.data.split(":") service = int(service) with GetDB() as db: @@ -897,7 +1011,7 @@ async def switch_service(call: types.CallbackQuery): else: selected_services.append(service) - mem_store.set(f'{call.message.chat.id}:services', selected_services) + mem_store.set(f"{call.message.chat.id}:services", selected_services) if action in ["edit"]: return await bot.edit_message_text( @@ -910,46 +1024,44 @@ async def switch_service(call: types.CallbackQuery): "edit", username=username, data_limit=mem_store.get(f"{call.message.chat.id}:data_limit"), - expire_date=mem_store.get(f"{call.message.chat.id}:expire_date")) + expire_date=mem_store.get(f"{call.message.chat.id}:expire_date"), + ), ) await bot.edit_message_text( call.message.text, call.message.chat.id, call.message.message_id, - reply_markup=BotKeyboard.select_services(services, selected_services, "create") + reply_markup=BotKeyboard.select_services(services, selected_services, "create"), ) -@bot.callback_query_handler(query_startswith('confirm:'), is_admin=True) +@bot.callback_query_handler(query_startswith("confirm:"), is_admin=True) async def confirm_user_command(call: types.CallbackQuery): - data = call.data.split(':')[1] + data = call.data.split(":")[1] chat_id = call.from_user.id full_name = call.from_user.full_name now = datetime.now() today = datetime( - year=now.year, - month=now.month, - day=now.day, - hour=23, - minute=59, - second=59) + year=now.year, month=now.month, day=now.day, hour=23, minute=59, second=59 + ) + @bot.callback_query_handler(query_startswith("do_delete"), is_admin=True) async def delete_user(call: types.CallbackQuery): - username = call.data.split(':')[1] + username = call.data.split(":")[1] with GetDB() as db: db_user = crud.get_user(db, username) crud.remove_user(db, db_user) await xray.operations.remove_user(db_user) await bot.edit_message_text( - '✅ User deleted.', + "✅ User deleted.", call.message.chat.id, call.message.message_id, - reply_markup=BotKeyboard.main_menu() + reply_markup=BotKeyboard.main_menu(), ) if TELEGRAM_LOGGER_CHANNEL_ID: - text = f'''\ + text = f"""\ 🗑 #Deleted #From_Bot ➖➖➖➖➖➖➖➖➖ Username : {db_user.username} @@ -957,9 +1069,9 @@ async def delete_user(call: types.CallbackQuery): Expire Date : \ {datetime.fromtimestamp(db_user.expire).strftime('%H:%M:%S %Y-%m-%d') if db_user.expire else "Never"} ➖➖➖➖➖➖➖➖➖ -By : {call.from_user.full_name}''' +By : {call.from_user.full_name}""" try: - await bot.send_message(TELEGRAM_LOGGER_CHANNEL_ID, text, 'HTML') + await bot.send_message(TELEGRAM_LOGGER_CHANNEL_ID, text, "HTML") except: pass @@ -969,79 +1081,81 @@ async def disable_user(call: types.CallbackQuery): username = call.data.split(":")[1] with GetDB() as db: db_user = crud.get_user(db, username) - crud.update_user(db, db_user, UserModify( - username=username, - status=UserStatusModify.disabled)) + crud.update_user( + db, db_user, UserModify(username=username, status=UserStatusModify.disabled) + ) await xray.operations.remove_user(db_user) user = UserResponse.model_validate(db_user) await bot.edit_message_text( get_user_info_text( - status='disabled', + status="disabled", username=username, sub_url=user.subscription_url, data_limit=db_user.data_limit, usage=db_user.used_traffic, expire=db_user.expire, - note=user.note + note=user.note, ), call.message.chat.id, call.message.message_id, - parse_mode='HTML', - reply_markup=BotKeyboard.user_menu(user_info={ - 'status': 'disabled', - 'username': db_user.username - }, note=True)) + parse_mode="HTML", + reply_markup=BotKeyboard.user_menu( + user_info={"status": "disabled", "username": db_user.username}, note=True + ), + ) if TELEGRAM_LOGGER_CHANNEL_ID: - text = f'''\ + text = f"""\ ❌ #Disabled #From_Bot ➖➖➖➖➖➖➖➖➖ Username : {username} ➖➖➖➖➖➖➖➖➖ -By : {call.from_user.full_name}''' +By : {call.from_user.full_name}""" try: - await bot.send_message(TELEGRAM_LOGGER_CHANNEL_ID, text, 'HTML') + await bot.send_message(TELEGRAM_LOGGER_CHANNEL_ID, text, "HTML") except: pass + @bot.callback_query_handler(query_startswith("do_activate"), is_admin=True) async def activate_user(call: types.CallbackQuery): username = call.data.split(":")[1] with GetDB() as db: db_user = crud.get_user(db, username) - crud.update_user(db, db_user, UserModify( - username=username, - status=UserStatusModify.active)) + crud.update_user( + db, db_user, UserModify(username=username, status=UserStatusModify.active) + ) await xray.operations.add_user(db_user) user = UserResponse.model_validate(db_user) await bot.edit_message_text( get_user_info_text( - status='active', + status="active", username=username, sub_url=user.subscription_url, data_limit=db_user.data_limit, usage=db_user.used_traffic, expire=db_user.expire, - note=user.note + note=user.note, ), call.message.chat.id, call.message.message_id, - parse_mode='HTML', - reply_markup=BotKeyboard.user_menu(user_info={ - 'status': 'active', - 'username': db_user.username - }, note=True)) + parse_mode="HTML", + reply_markup=BotKeyboard.user_menu( + user_info={"status": "active", "username": db_user.username}, note=True + ), + ) if TELEGRAM_LOGGER_CHANNEL_ID: - text = f'''\ + text = f"""\ ✅ #Activated #From_Bot ➖➖➖➖➖➖➖➖➖ Username : {username} ➖➖➖➖➖➖➖➖➖ -By : {call.from_user.full_name}''' +By : {call.from_user.full_name}""" try: - await bot.send_message(TELEGRAM_LOGGER_CHANNEL_ID, text, 'HTML') + await bot.send_message(TELEGRAM_LOGGER_CHANNEL_ID, text, "HTML") except: pass + @bot.callback_query_handler(query_startswith("do_reset_usage"), is_admin=True) async def reset_all_usages(call: types.CallbackQuery): username = call.data.split(":")[1] @@ -1057,24 +1171,24 @@ async def reset_all_usages(call: types.CallbackQuery): data_limit=user.data_limit, usage=user.used_traffic, expire=user.expire, - note=user.note + note=user.note, ), call.message.chat.id, call.message.message_id, - parse_mode='HTML', - reply_markup=BotKeyboard.user_menu(user_info={ - 'status': user.status, - 'username': user.username - }, note=True)) + parse_mode="HTML", + reply_markup=BotKeyboard.user_menu( + user_info={"status": user.status, "username": user.username}, note=True + ), + ) if TELEGRAM_LOGGER_CHANNEL_ID: - text = f'''\ + text = f"""\ 🔁 #Reset_usage #From_Bot ➖➖➖➖➖➖➖➖➖ Username : {username} ➖➖➖➖➖➖➖➖➖ -By : {call.from_user.full_name}''' +By : {call.from_user.full_name}""" try: - await bot.send_message(TELEGRAM_LOGGER_CHANNEL_ID, text, 'HTML') + await bot.send_message(TELEGRAM_LOGGER_CHANNEL_ID, text, "HTML") except: pass @@ -1082,46 +1196,48 @@ async def reset_all_usages(call: types.CallbackQuery): @bot.callback_query_handler(query_equals("do_restart"), is_admin=True) async def restart_xray(call: types.CallbackQuery): m = await bot.edit_message_text( - '🔄 Restarting XRay core...', call.message.chat.id, call.message.message_id) + "🔄 Restarting XRay core...", call.message.chat.id, call.message.message_id + ) await xray.core.restart(xray.config.include_db_users()) for node_id, node in list(xray.nodes.items()): if await node.is_healthy(): await xray.operations.restart_node(node_id, xray.config.include_db_users()) await bot.edit_message_text( - '✅ XRay core restarted successfully.', - m.chat.id, m.message_id, - reply_markup=BotKeyboard.main_menu() + "✅ XRay core restarted successfully.", + m.chat.id, + m.message_id, + reply_markup=BotKeyboard.main_menu(), ) + @bot.callback_query_handler(query_equals("edit_user_done"), is_admin=True) async def edit_user(call: types.CallbackQuery): - if (username := mem_store.get(f'{call.message.chat.id}:username')) is None: + if (username := mem_store.get(f"{call.message.chat.id}:username")) is None: try: - await bot.delete_message(call.message.chat.id, - call.message.message_id) + await bot.delete_message(call.message.chat.id, call.message.message_id) except Exception: pass return await bot.send_message( call.message.chat.id, - '❌ Bot reload detected. Please start over.', - reply_markup=BotKeyboard.main_menu() + "❌ Bot reload detected. Please start over.", + reply_markup=BotKeyboard.main_menu(), ) - if not mem_store.get(f'{call.message.chat.id}:services'): + if not mem_store.get(f"{call.message.chat.id}:services"): return await bot.answer_callback_query( - call.id, - '❌ No Service selected.', - show_alert=True + call.id, "❌ No Service selected.", show_alert=True ) - #inbounds: dict[str, list[str]] = { + # inbounds: dict[str, list[str]] = { # k: v for k, v in mem_store.get(f'{call.message.chat.id}:protocols').items() if v} services: List[int] = mem_store.get(f"{call.message.chat.id}:services") with GetDB() as db: db_user = crud.get_user(db, username) if not db_user: - return await bot.answer_callback_query(call.id, text=f"User not found!", show_alert=True) + return await bot.answer_callback_query( + call.id, text=f"User not found!", show_alert=True + ) # proxies = {p.type.value: p.settings for p in db_user.proxies} @@ -1135,10 +1251,14 @@ async def edit_user(call: types.CallbackQuery): modify = UserModify( username=username, # status=db_user.status, - expire=int(mem_store.get(f'{call.message.chat.id}:expire_date').timestamp()) - if mem_store.get(f'{call.message.chat.id}:expire_date') else 0, + expire=( + int(mem_store.get(f"{call.message.chat.id}:expire_date").timestamp()) + if mem_store.get(f"{call.message.chat.id}:expire_date") + else 0 + ), data_limit=mem_store.get(f"{call.message.chat.id}:data_limit"), - services=services) + services=services, + ) last_user = UserResponse.model_validate(db_user) db_user = crud.update_user(db, db_user, modify) @@ -1158,88 +1278,90 @@ async def edit_user(call: types.CallbackQuery): data_limit=user.data_limit, usage=user.used_traffic, expire=user.expire, - note=user.note + note=user.note, ) await bot.edit_message_text( text, call.message.chat.id, call.message.message_id, parse_mode="HTML", - reply_markup=BotKeyboard.user_menu({ - 'username': db_user.username, - 'status': db_user.status}, - note=True) + reply_markup=BotKeyboard.user_menu( + {"username": db_user.username, "status": db_user.status}, note=True + ), ) if TELEGRAM_LOGGER_CHANNEL_ID: tag = f'\n➖➖➖➖➖➖➖➖➖ \nBy : {call.from_user.full_name}' if last_user.data_limit != user.data_limit: - text = f'''\ + text = f"""\ đŸ“ļ #Traffic_Change #From_Bot ➖➖➖➖➖➖➖➖➖ Username : {user.username} Last Traffic Limit : {readable_size(last_user.data_limit) if last_user.data_limit else "Unlimited"} -New Traffic Limit : {readable_size(user.data_limit) if user.data_limit else "Unlimited"}{tag}''' +New Traffic Limit : {readable_size(user.data_limit) if user.data_limit else "Unlimited"}{tag}""" try: - await bot.send_message(TELEGRAM_LOGGER_CHANNEL_ID, text, 'HTML') + await bot.send_message(TELEGRAM_LOGGER_CHANNEL_ID, text, "HTML") except: pass if last_user.expire != user.expire: - text = f'''\ + text = f"""\ 📅 #Expiry_Change #From_Bot ➖➖➖➖➖➖➖➖➖ Username : {user.username} Last Expire Date : \ {datetime.fromtimestamp(last_user.expire).strftime('%H:%M:%S %Y-%m-%d') if last_user.expire else "Never"} New Expire Date : \ -{datetime.fromtimestamp(user.expire).strftime('%H:%M:%S %Y-%m-%d') if user.expire else "Never"}{tag}''' +{datetime.fromtimestamp(user.expire).strftime('%H:%M:%S %Y-%m-%d') if user.expire else "Never"}{tag}""" try: - await bot.send_message(TELEGRAM_LOGGER_CHANNEL_ID, text, 'HTML') + await bot.send_message(TELEGRAM_LOGGER_CHANNEL_ID, text, "HTML") except: pass if list(last_user.inbounds.values())[0] != list(user.inbounds.values())[0]: - text = f'''\ + text = f"""\ âš™ī¸ #Inbounds_Change #From_Bot ➖➖➖➖➖➖➖➖➖ Username : {user.username} Last Proxies : {", ".join(list(last_user.inbounds.values())[0])} -New Proxies : {", ".join(list(user.inbounds.values())[0])}{tag}''' +New Proxies : {", ".join(list(user.inbounds.values())[0])}{tag}""" try: - await bot.send_message(TELEGRAM_LOGGER_CHANNEL_ID, text, 'HTML') + await bot.send_message(TELEGRAM_LOGGER_CHANNEL_ID, text, "HTML") except: pass - @bot.callback_query_handler(query_startswith("add_user_done"), is_admin=True) async def delete_limpired(call: types.CallbackQuery): - if mem_store.get(f'{call.message.chat.id}:username') is None: + if mem_store.get(f"{call.message.chat.id}:username") is None: try: - await bot.delete_message(call.message.chat.id, - call.message.message_id) + await bot.delete_message(call.message.chat.id, call.message.message_id) except Exception: pass return await bot.send_message( call.message.chat.id, - '❌ Bot reload detected. Please start over.', - reply_markup=BotKeyboard.main_menu() + "❌ Bot reload detected. Please start over.", + reply_markup=BotKeyboard.main_menu(), ) - if not mem_store.get(f'{call.message.chat.id}:services'): + if not mem_store.get(f"{call.message.chat.id}:services"): return await bot.answer_callback_query( - call.id, - '❌ No services selected.', - show_alert=True + call.id, "❌ No services selected.", show_alert=True ) - services: List[int] = mem_store.get(f'{call.message.chat.id}:services', []) - + services: List[int] = mem_store.get(f"{call.message.chat.id}:services", []) + new_user = UserCreate( - username=mem_store.get(f'{call.message.chat.id}:username'), - expire=int(mem_store.get(f'{call.message.chat.id}:expire_date').timestamp()) - if mem_store.get(f'{call.message.chat.id}:expire_date') else None, - data_limit=mem_store.get(f'{call.message.chat.id}:data_limit') - if mem_store.get(f'{call.message.chat.id}:data_limit') else None, - services=services) + username=mem_store.get(f"{call.message.chat.id}:username"), + expire=( + int(mem_store.get(f"{call.message.chat.id}:expire_date").timestamp()) + if mem_store.get(f"{call.message.chat.id}:expire_date") + else None + ), + data_limit=( + mem_store.get(f"{call.message.chat.id}:data_limit") + if mem_store.get(f"{call.message.chat.id}:data_limit") + else None + ), + services=services, + ) try: with GetDB() as db: db_user = crud.create_user(db, new_user) @@ -1248,9 +1370,7 @@ async def delete_limpired(call: types.CallbackQuery): except sqlalchemy.exc.IntegrityError: db.rollback() return await bot.answer_callback_query( - call.id, - '❌ Username already exists.', - show_alert=True + call.id, "❌ Username already exists.", show_alert=True ) await xray.operations.add_user(db_user) @@ -1262,17 +1382,20 @@ async def delete_limpired(call: types.CallbackQuery): data_limit=user.data_limit, usage=user.used_traffic, expire=user.expire, - note=user.note + note=user.note, ) await bot.edit_message_text( text, call.message.chat.id, call.message.message_id, parse_mode="HTML", - reply_markup=BotKeyboard.user_menu(user_info={'status': user.status, 'username': user.username}, note=True)) + reply_markup=BotKeyboard.user_menu( + user_info={"status": user.status, "username": user.username}, note=True + ), + ) if TELEGRAM_LOGGER_CHANNEL_ID: - text = f'''\ + text = f"""\ 🆕 #Created #From_Bot ➖➖➖➖➖➖➖➖➖ Username : {user.username} @@ -1281,161 +1404,216 @@ async def delete_limpired(call: types.CallbackQuery): {datetime.fromtimestamp(user.expire).strftime('%H:%M:%S %Y-%m-%d') if user.expire else "Never"} Services : {"" if not services else ", ".join([s.name for s in services])} ➖➖➖➖➖➖➖➖➖ -By : {call.from_user.full_name}''' +By : {call.from_user.full_name}""" try: - await bot.send_message(TELEGRAM_LOGGER_CHANNEL_ID, text, 'HTML') + await bot.send_message(TELEGRAM_LOGGER_CHANNEL_ID, text, "HTML") except: pass -@bot.callback_query_handler(query_startswith(['do_delete_expired', 'do_delete_limited']), is_admin=True) +@bot.callback_query_handler( + query_startswith(["do_delete_expired", "do_delete_limited"]), is_admin=True +) async def delete_limpired(call: types.CallbackQuery): await bot.edit_message_text( - 'âŗ In Progress...', + "âŗ In Progress...", call.message.chat.id, call.message.message_id, - parse_mode="HTML") + parse_mode="HTML", + ) with GetDB() as db: depleted_users = crud.get_users( - db, status=[UserStatus.limited if data == 'delete_limited' else UserStatus.expired]) - file_name = f'{data[8:]}_users_{int(now.timestamp()*1000)}.txt' - with open(file_name, 'w') as f: - f.write('USERNAME\tEXIPRY\tUSAGE/LIMIT\tSTATUS\n') + db, + status=[ + UserStatus.limited if data == "delete_limited" else UserStatus.expired + ], + ) + file_name = f"{data[8:]}_users_{int(now.timestamp()*1000)}.txt" + with open(file_name, "w") as f: + f.write("USERNAME\tEXIPRY\tUSAGE/LIMIT\tSTATUS\n") deleted = 0 for user in depleted_users: try: crud.remove_user(db, user) await xray.operations.remove_user(user) - deleted +=1 - f.write(\ -f'{user.username}\ + deleted += 1 + f.write( + f'{user.username}\ \t{datetime.fromtimestamp(user.expire) if user.expire else "never"}\ \t{readable_size(user.used_traffic) if user.used_traffic else 0}\ /{readable_size(user.data_limit) if user.data_limit else "Unlimited"}\ -\t{user.status}\n') +\t{user.status}\n' + ) except: db.rollback() await bot.edit_message_text( - f'✅ {deleted}/{len(depleted_users)} {data[7:].title()} Users Deleted', + f"✅ {deleted}/{len(depleted_users)} {data[7:].title()} Users Deleted", call.message.chat.id, call.message.message_id, parse_mode="HTML", - reply_markup=BotKeyboard.main_menu()) + reply_markup=BotKeyboard.main_menu(), + ) if TELEGRAM_LOGGER_CHANNEL_ID: - text = f'''\ + text = f"""\ 🗑 #Delete #{data[7:].title()} #From_Bot ➖➖➖➖➖➖➖➖➖ Count: {deleted} ➖➖➖➖➖➖➖➖➖ -By : {call.from_user.full_name}''' +By : {call.from_user.full_name}""" try: - await bot.send_document(TELEGRAM_LOGGER_CHANNEL_ID, open(file_name, 'rb'), caption=text, parse_mode='HTML') + await bot.send_document( + TELEGRAM_LOGGER_CHANNEL_ID, + open(file_name, "rb"), + caption=text, + parse_mode="HTML", + ) os.remove(file_name) except: pass -@bot.callback_query_handler(query_startswith('do_add_data:'), is_admin=True) + +@bot.callback_query_handler(query_startswith("do_add_data:"), is_admin=True) async def add_data_to_all(call: types.CallbackQuery): schedule_delete_message( call.message.chat.id, - (await bot.send_message(chat_id, 'âŗ In Progress...', 'HTML')).id) + (await bot.send_message(chat_id, "âŗ In Progress...", "HTML")).id, + ) data_limit = float(call.data.split(":")[1]) * 1024 * 1024 * 1024 with GetDB() as db: users = crud.get_users(db) counter = 0 - file_name = f'new_data_limit_users_{int(now.timestamp()*1000)}.txt' - with open(file_name, 'w') as f: - f.write('USERNAME\tEXIPRY\tUSAGE/LIMIT\tSTATUS\n') + file_name = f"new_data_limit_users_{int(now.timestamp()*1000)}.txt" + with open(file_name, "w") as f: + f.write("USERNAME\tEXIPRY\tUSAGE/LIMIT\tSTATUS\n") for user in users: try: - if user.data_limit and user.status not in [UserStatus.limited, UserStatus.expired]: - user = crud.update_user(db, user, UserModify(username=user.username, data_limit=(user.data_limit + data_limit))) + if user.data_limit and user.status not in [ + UserStatus.limited, + UserStatus.expired, + ]: + user = crud.update_user( + db, + user, + UserModify( + username=user.username, + data_limit=(user.data_limit + data_limit), + ), + ) counter += 1 f.write( f'{user.username}\ \t{datetime.fromtimestamp(user.expire) if user.expire else "never"}\ \t{readable_size(user.used_traffic) if user.used_traffic else 0}\ /{readable_size(user.data_limit) if user.data_limit else "Unlimited"}\ -\t{user.status}\n') +\t{user.status}\n' + ) except: db.rollback() await cleanup_messages(chat_id) await bot.send_message( chat_id, f'✅ {counter}/{len(users)} Users Data Limit according to {"+" if data_limit > 0 else "-"}{readable_size(abs(data_limit))}', - 'HTML', - reply_markup=BotKeyboard.main_menu()) + "HTML", + reply_markup=BotKeyboard.main_menu(), + ) if TELEGRAM_LOGGER_CHANNEL_ID: - text = f'''\ + text = f"""\ đŸ“ļ #Traffic_Change #From_Bot ➖➖➖➖➖➖➖➖➖ According to: {"+" if data_limit > 0 else "-"}{readable_size(abs(data_limit))} Count: {counter} ➖➖➖➖➖➖➖➖➖ -By : {call.from_user.full_name}''' +By : {call.from_user.full_name}""" try: - await bot.send_document(TELEGRAM_LOGGER_CHANNEL_ID, open(file_name, 'rb'), caption=text, parse_mode='HTML') + await bot.send_document( + TELEGRAM_LOGGER_CHANNEL_ID, + open(file_name, "rb"), + caption=text, + parse_mode="HTML", + ) os.remove(file_name) except: pass -@bot.callback_query_handler(query_startswith('do_add_time:'), is_admin=True) + +@bot.callback_query_handler(query_startswith("do_add_time:"), is_admin=True) async def add_time_to_all(call: types.CallbackQuery): schedule_delete_message( call.message.chat.id, - (await bot.send_message(chat_id, 'âŗ In Progress...', 'HTML')).id) + (await bot.send_message(chat_id, "âŗ In Progress...", "HTML")).id, + ) days = int(call.data.split(":")[1]) with GetDB() as db: users = crud.get_users(db) counter = 0 - file_name = f'new_expiry_users_{int(now.timestamp()*1000)}.txt' - with open(file_name, 'w') as f: - f.write('USERNAME\tEXIPRY\tUSAGE/LIMIT\tSTATUS\n') + file_name = f"new_expiry_users_{int(now.timestamp()*1000)}.txt" + with open(file_name, "w") as f: + f.write("USERNAME\tEXIPRY\tUSAGE/LIMIT\tSTATUS\n") for user in users: try: - if user.expire and user.status not in [UserStatus.limited, UserStatus.expired]: + if user.expire and user.status not in [ + UserStatus.limited, + UserStatus.expired, + ]: user = crud.update_user( - db, user, + db, + user, UserModify( username=user.username, expire=int( - (datetime.fromtimestamp(user.expire) + relativedelta(days=days)).timestamp()))) + ( + datetime.fromtimestamp(user.expire) + + relativedelta(days=days) + ).timestamp() + ), + ), + ) counter += 1 f.write( f'{user.username}\ \t{datetime.fromtimestamp(user.expire) if user.expire else "never"}\ \t{readable_size(user.used_traffic) if user.used_traffic else 0}\ /{readable_size(user.data_limit) if user.data_limit else "Unlimited"}\ -\t{user.status}\n') +\t{user.status}\n' + ) except: db.rollback() await cleanup_messages(chat_id) await bot.send_message( chat_id, - f'✅ {counter}/{len(users)} Users Expiry Changes according to {days} Days', - 'HTML', - reply_markup=BotKeyboard.main_menu()) + f"✅ {counter}/{len(users)} Users Expiry Changes according to {days} Days", + "HTML", + reply_markup=BotKeyboard.main_menu(), + ) if TELEGRAM_LOGGER_CHANNEL_ID: - text = f'''\ + text = f"""\ 📅 #Expiry_Change #From_Bot ➖➖➖➖➖➖➖➖➖ According to: {days} Days Count: {counter} ➖➖➖➖➖➖➖➖➖ -By : {call.from_user.full_name}''' +By : {call.from_user.full_name}""" try: - await bot.send_document(TELEGRAM_LOGGER_CHANNEL_ID, open(file_name, 'rb'), caption=text, parse_mode='HTML') + await bot.send_document( + TELEGRAM_LOGGER_CHANNEL_ID, + open(file_name, "rb"), + caption=text, + parse_mode="HTML", + ) os.remove(file_name) except: pass -@bot.callback_query_handler(query_startswith('do_revoke:'), is_admin=True) + +@bot.callback_query_handler(query_startswith("do_revoke:"), is_admin=True) async def revoke_sub(call: types.CallbackQuery): username = call.data.split(":")[1] with GetDB() as db: db_user = crud.get_user(db, username) if not db_user: - return await bot.answer_callback_query(call.id, text=f"User not found!", show_alert=True) + return await bot.answer_callback_query( + call.id, text=f"User not found!", show_alert=True + ) db_user = crud.revoke_user_sub(db, db_user) user = UserResponse.model_validate(db_user) text = get_user_info_text( @@ -1445,23 +1623,27 @@ async def revoke_sub(call: types.CallbackQuery): expire=user.expire, data_limit=user.data_limit, usage=user.used_traffic, - note=user.note) + note=user.note, + ) await bot.edit_message_text( - f'✅ Subscription Successfully Revoked!\n\n{text}', - call.message.chat.id, - call.message.message_id, - parse_mode="HTML", - reply_markup=BotKeyboard.user_menu(user_info={'status': user.status, 'username': user.username}, note=True)) + f"✅ Subscription Successfully Revoked!\n\n{text}", + call.message.chat.id, + call.message.message_id, + parse_mode="HTML", + reply_markup=BotKeyboard.user_menu( + user_info={"status": user.status, "username": user.username}, note=True + ), + ) if TELEGRAM_LOGGER_CHANNEL_ID: - text = f'''\ + text = f"""\ đŸšĢ #Revoke_sub #From_Bot ➖➖➖➖➖➖➖➖➖ Username: {username} ➖➖➖➖➖➖➖➖➖ -By : {call.from_user.full_name}''' +By : {call.from_user.full_name}""" try: - await bot.send_message(TELEGRAM_LOGGER_CHANNEL_ID, text, 'HTML') + await bot.send_message(TELEGRAM_LOGGER_CHANNEL_ID, text, "HTML") except: pass @@ -1471,7 +1653,7 @@ async def search(message: types.Message): with GetDB() as db: db_user = crud.get_user(db, message.text) if not db_user: - return await bot.reply_to(message, '❌ User not found.') + return await bot.reply_to(message, "❌ User not found.") user = UserResponse.model_validate(db_user) text = get_user_info_text( status=user.status, @@ -1480,8 +1662,13 @@ async def search(message: types.Message): expire=user.expire, data_limit=user.data_limit, usage=user.used_traffic, - note=user.note) - return await bot.reply_to(message, text, parse_mode="html", reply_markup=BotKeyboard.user_menu(user_info={ - 'status': user.status, - 'username': user.username - }, note=True)) + note=user.note, + ) + return await bot.reply_to( + message, + text, + parse_mode="html", + reply_markup=BotKeyboard.user_menu( + user_info={"status": user.status, "username": user.username}, note=True + ), + ) diff --git a/app/telegram/handlers/report.py b/app/telegram/handlers/report.py index 951ad32..2b12af2 100644 --- a/app/telegram/handlers/report.py +++ b/app/telegram/handlers/report.py @@ -17,16 +17,27 @@ async def report(message: str, parse_mode="html", keyboard=None): if bot and (TELEGRAM_ADMIN_ID or TELEGRAM_LOGGER_CHANNEL_ID): try: if TELEGRAM_LOGGER_CHANNEL_ID: - await bot.send_message(TELEGRAM_LOGGER_CHANNEL_ID, message, parse_mode=parse_mode) + await bot.send_message( + TELEGRAM_LOGGER_CHANNEL_ID, message, parse_mode=parse_mode + ) else: for admin in TELEGRAM_ADMIN_ID: - await bot.send_message(admin, message, parse_mode=parse_mode, reply_markup=keyboard) + await bot.send_message( + admin, message, parse_mode=parse_mode, reply_markup=keyboard + ) except ApiTelegramException as e: logger.error(e) -async def report_new_user(user_id: int, username: str, by: str, expire_date: int, data_limit: int, services: list): - text = '''\ +async def report_new_user( + user_id: int, + username: str, + by: str, + expire_date: int, + data_limit: int, + services: list, +): + text = """\ 🆕 #Created ➖➖➖➖➖➖➖➖➖ Username : {username} @@ -34,26 +45,30 @@ async def report_new_user(user_id: int, username: str, by: str, expire_date: int Expire Date : {expire_date} Services : {services} ➖➖➖➖➖➖➖➖➖ -By : #{by}'''.format( +By : #{by}""".format( by=escape_html(by), username=escape_html(username), data_limit=readable_size(data_limit) if data_limit else "Unlimited", - expire_date=datetime.fromtimestamp(expire_date).strftime("%H:%M:%S %Y-%m-%d") if expire_date else "Never", - services="" if not services else ", ".join([s.name for s in services]) + expire_date=( + datetime.fromtimestamp(expire_date).strftime("%H:%M:%S %Y-%m-%d") + if expire_date + else "Never" + ), + services="" if not services else ", ".join([s.name for s in services]), ) return await report( text, - keyboard=BotKeyboard.user_menu({ - 'username': username, - 'id': user_id, - 'status': 'active' - }, with_back=False) + keyboard=BotKeyboard.user_menu( + {"username": username, "id": user_id, "status": "active"}, with_back=False + ), ) -async def report_user_modification(username: str, expire_date: int, data_limit: int, services: list, by: str): - text = '''\ +async def report_user_modification( + username: str, expire_date: int, data_limit: int, services: list, by: str +): + text = """\ âœī¸ #Modified ➖➖➖➖➖➖➖➖➖ Username : {username} @@ -62,48 +77,52 @@ async def report_user_modification(username: str, expire_date: int, data_limit: Services : {services} ➖➖➖➖➖➖➖➖➖ By : #{by}\ - '''.format( + """.format( by=escape_html(by), username=escape_html(username), data_limit=readable_size(data_limit) if data_limit else "Unlimited", - expire_date=datetime.fromtimestamp(expire_date).strftime("%H:%M:%S %Y-%m-%d") if expire_date else "Never", - services=", ".join([s.name for s in services]) + expire_date=( + datetime.fromtimestamp(expire_date).strftime("%H:%M:%S %Y-%m-%d") + if expire_date + else "Never" + ), + services=", ".join([s.name for s in services]), ) - return await report(text, keyboard=BotKeyboard.user_menu({ - 'username': username, - 'status': 'active' - }, with_back=False)) + return await report( + text, + keyboard=BotKeyboard.user_menu( + {"username": username, "status": "active"}, with_back=False + ), + ) async def report_user_deletion(username: str, by: str): - text = '''\ + text = """\ 🗑 #Deleted ➖➖➖➖➖➖➖➖➖ Username : {username} ➖➖➖➖➖➖➖➖➖ By : #{by}\ - '''.format( - by=escape_html(by), - username=escape_html(username) + """.format( + by=escape_html(by), username=escape_html(username) ) return await report(text) async def report_status_change(username: str, status: str): _status = { - 'active': '✅ #Activated', - 'disabled': '❌ #Disabled', - 'limited': 'đŸĒĢ #Limited', - 'expired': '🕔 #Expired' + "active": "✅ #Activated", + "disabled": "❌ #Disabled", + "limited": "đŸĒĢ #Limited", + "expired": "🕔 #Expired", } - text = '''\ + text = """\ {status} ➖➖➖➖➖➖➖➖➖ Username : {username}\ - '''.format( - username=escape_html(username), - status=_status[status] + """.format( + username=escape_html(username), status=_status[status] ) return await report(text) @@ -116,8 +135,7 @@ async def report_user_usage_reset(username: str, by: str): ➖➖➖➖➖➖➖➖➖ By : #{by}\ """.format( - by=escape_html(by), - username=escape_html(username) + by=escape_html(by), username=escape_html(username) ) return await report(text) @@ -131,8 +149,7 @@ async def report_user_subscription_revoked(username: str, by: str): ➖➖➖➖➖➖➖➖➖ By : #{by}\ """.format( - by=escape_html(by), - username=escape_html(username) + by=escape_html(by), username=escape_html(username) ) return await report(text) diff --git a/app/telegram/handlers/user.py b/app/telegram/handlers/user.py index 82c8a70..696b2e9 100644 --- a/app/telegram/handlers/user.py +++ b/app/telegram/handlers/user.py @@ -2,6 +2,7 @@ from datetime import datetime from app.db import GetDB, crud from app.models.user import UserResponse from app.telegram import bot + # from pytz import UTC from telebot.custom_filters import ChatFilter from telebot.util import extract_arguments @@ -11,11 +12,13 @@ from app.utils.system import readable_size bot.add_custom_filter(ChatFilter()) -@bot.message_handler(commands=['usage']) +@bot.message_handler(commands=["usage"]) def usage_command(message): username = extract_arguments(message.text) if not username: - return bot.reply_to(message, 'Usage: `/usage `', parse_mode='MarkdownV2') + return bot.reply_to( + message, "Usage: `/usage `", parse_mode="MarkdownV2" + ) with GetDB() as db: dbuser = crud.get_user(db, username) @@ -24,13 +27,9 @@ def usage_command(message): return bot.reply_to(message, "No user found with this username") user = UserResponse.model_validate(dbuser) - statuses = { - 'active': '✅', - 'expired': '🕰', - 'limited': 'đŸ“ĩ', - 'disabled': '❌'} + statuses = {"active": "✅", "expired": "🕰", "limited": "đŸ“ĩ", "disabled": "❌"} - text = f'''\ + text = f"""\ ┌─{statuses[user.status]} Status: {user.status.title()} │ └─Username: {user.username} │ @@ -38,6 +37,6 @@ def usage_command(message): │ └─Data Used: {readable_size(user.used_traffic) if user.used_traffic else "-"} │ └─📅 Expiry Date: {datetime.fromtimestamp(user.expire).date() if user.expire else 'Never'} - └─Days left: {(datetime.fromtimestamp(user.expire or 0) - datetime.now()).days if user.expire else '-'}''' + └─Days left: {(datetime.fromtimestamp(user.expire or 0) - datetime.now()).days if user.expire else '-'}""" - return bot.reply_to(message, text, parse_mode='HTML') + return bot.reply_to(message, text, parse_mode="HTML") diff --git a/app/telegram/utils/custom_filters.py b/app/telegram/utils/custom_filters.py index c8965ae..9d0fc82 100644 --- a/app/telegram/utils/custom_filters.py +++ b/app/telegram/utils/custom_filters.py @@ -8,7 +8,7 @@ from config import TELEGRAM_ADMIN_ID class IsAdminFilter(AdvancedCustomFilter): - key = 'is_admin' + key = "is_admin" async def check(self, message, text): """ diff --git a/app/telegram/utils/keyboard.py b/app/telegram/utils/keyboard.py index 2a216d0..421f990 100644 --- a/app/telegram/utils/keyboard.py +++ b/app/telegram/utils/keyboard.py @@ -19,107 +19,122 @@ class BotKeyboard: def main_menu(): keyboard = types.InlineKeyboardMarkup() keyboard.add( - types.InlineKeyboardButton(text='🔁 System Info', callback_data='system'), - types.InlineKeyboardButton(text='â™ģī¸ Restart Xray', callback_data='restart')) + types.InlineKeyboardButton(text="🔁 System Info", callback_data="system"), + types.InlineKeyboardButton(text="â™ģī¸ Restart Xray", callback_data="restart"), + ) keyboard.add( - types.InlineKeyboardButton(text='đŸ‘Ĩ Users', callback_data='users:1'), - types.InlineKeyboardButton(text='âœī¸ Edit All Users', callback_data='edit_all')) - #keyboard.add( + types.InlineKeyboardButton(text="đŸ‘Ĩ Users", callback_data="users:1"), + types.InlineKeyboardButton( + text="âœī¸ Edit All Users", callback_data="edit_all" + ), + ) + # keyboard.add( # types.InlineKeyboardButton(text='➕ Create User From Template', callback_data='template_add_user')) keyboard.add( - types.InlineKeyboardButton(text='➕ Create User', callback_data='add_user')) + types.InlineKeyboardButton(text="➕ Create User", callback_data="add_user") + ) return keyboard - @staticmethod def edit_all_menu(): keyboard = types.InlineKeyboardMarkup() keyboard.add( - types.InlineKeyboardButton(text='🗑 Delete Expired', callback_data='delete_expired'), - types.InlineKeyboardButton(text='🗑 Delete Limited', callback_data='delete_limited')) + types.InlineKeyboardButton( + text="🗑 Delete Expired", callback_data="delete_expired" + ), + types.InlineKeyboardButton( + text="🗑 Delete Limited", callback_data="delete_limited" + ), + ) keyboard.add( - types.InlineKeyboardButton(text='🔋 Data (➕|➖)', callback_data='add_data'), - types.InlineKeyboardButton(text='📅 Time (➕|➖)', callback_data='add_time')) - #keyboard.add( + types.InlineKeyboardButton( + text="🔋 Data (➕|➖)", callback_data="add_data" + ), + types.InlineKeyboardButton( + text="📅 Time (➕|➖)", callback_data="add_time" + ), + ) + # keyboard.add( # types.InlineKeyboardButton(text='➕ Add Inbound', callback_data='inbound_add'), # types.InlineKeyboardButton(text='➖ Remove Inbound', callback_data='inbound_remove')) - keyboard.add(types.InlineKeyboardButton(text='🔙 Back', callback_data='cancel')) + keyboard.add(types.InlineKeyboardButton(text="🔙 Back", callback_data="cancel")) return keyboard - - #@staticmethod - #def inbounds_menu(action, inbounds): + # @staticmethod + # def inbounds_menu(action, inbounds): # keyboard = types.InlineKeyboardMarkup() # for inbound in inbounds: # keyboard.add(types.InlineKeyboardButton(text=inbound, callback_data=f'confirm_{action}:{inbound}')) # keyboard.add(types.InlineKeyboardButton(text='🔙 Back', callback_data='cancel')) # return keyboard - @staticmethod - def random_username(template_id: str = ''): + def random_username(template_id: str = ""): keyboard = types.InlineKeyboardMarkup() - keyboard.add(types.InlineKeyboardButton( - text='🔡 Random Username', - callback_data=f'random:{template_id}')) - keyboard.add(types.InlineKeyboardButton( - text='🔙 Cancel', - callback_data='cancel')) + keyboard.add( + types.InlineKeyboardButton( + text="🔡 Random Username", callback_data=f"random:{template_id}" + ) + ) + keyboard.add( + types.InlineKeyboardButton(text="🔙 Cancel", callback_data="cancel") + ) return keyboard - @staticmethod def user_menu(user_info, with_back: bool = True, page: int = 1, note: bool = False): keyboard = types.InlineKeyboardMarkup() keyboard.add( types.InlineKeyboardButton( - text='❌ Disable' if user_info['status'] == 'active' else '✅ Activate', - callback_data=f"{'suspend' if user_info['status'] == 'active' else 'activate'}:{user_info['username']}" + text="❌ Disable" if user_info["status"] == "active" else "✅ Activate", + callback_data=f"{'suspend' if user_info['status'] == 'active' else 'activate'}:{user_info['username']}", ), types.InlineKeyboardButton( - text='🗑 Delete', - callback_data=f"delete:{user_info['username']}" + text="🗑 Delete", callback_data=f"delete:{user_info['username']}" ), ) if note: keyboard.add( types.InlineKeyboardButton( - text='đŸšĢ Revoke Sub', - callback_data=f"revoke_sub:{user_info['username']}"), + text="đŸšĢ Revoke Sub", + callback_data=f"revoke_sub:{user_info['username']}", + ), types.InlineKeyboardButton( - text='âœī¸ Edit', - callback_data=f"edit:{user_info['username']}")) + text="âœī¸ Edit", callback_data=f"edit:{user_info['username']}" + ), + ) keyboard.add( types.InlineKeyboardButton( - text='📝 Edit Note', - callback_data=f"edit_note:{user_info['username']}"), + text="📝 Edit Note", + callback_data=f"edit_note:{user_info['username']}", + ), types.InlineKeyboardButton( - text='📡 Links', - callback_data=f"links:{user_info['username']}")) + text="📡 Links", callback_data=f"links:{user_info['username']}" + ), + ) else: keyboard.add( types.InlineKeyboardButton( - text='📡 Links', - callback_data=f"links:{user_info['username']}"), + text="📡 Links", callback_data=f"links:{user_info['username']}" + ), types.InlineKeyboardButton( - text='âœī¸ Edit', - callback_data=f"edit:{user_info['username']}")) + text="âœī¸ Edit", callback_data=f"edit:{user_info['username']}" + ), + ) keyboard.add( types.InlineKeyboardButton( - text='🔁 Reset usage', - callback_data=f"reset_usage:{user_info['username']}" + text="🔁 Reset usage", + callback_data=f"reset_usage:{user_info['username']}", ), types.InlineKeyboardButton( - text='🔋 Charge', - callback_data=f"charge:{user_info['username']}" - ) + text="🔋 Charge", callback_data=f"charge:{user_info['username']}" + ), ) if with_back: keyboard.add( types.InlineKeyboardButton( - text='🔙 Back', - callback_data=f'users:{page}' + text="🔙 Back", callback_data=f"users:{page}" ) ) return keyboard @@ -130,41 +145,31 @@ class BotKeyboard: keyboard.add( types.InlineKeyboardButton( - text="đŸ–ŧ QR code", - callback_data=f'genqr:{username}' + text="đŸ–ŧ QR code", callback_data=f"genqr:{username}" ) ) keyboard.add( - types.InlineKeyboardButton( - text='🔙 Back', - callback_data=f'user:{username}' - ) + types.InlineKeyboardButton(text="🔙 Back", callback_data=f"user:{username}") ) return keyboard - @staticmethod def subscription_page(sub_url: str): keyboard = types.InlineKeyboardMarkup() - if sub_url[:4] == 'http': - keyboard.add(types.InlineKeyboardButton( - text='🚀 Subscription Page', - url=sub_url)) + if sub_url[:4] == "http": + keyboard.add( + types.InlineKeyboardButton(text="🚀 Subscription Page", url=sub_url) + ) return keyboard - @staticmethod def confirm_action(action: str, username: str = None): keyboard = types.InlineKeyboardMarkup() keyboard.add( types.InlineKeyboardButton( - text='Yes', - callback_data=f"{action}:{username}" + text="Yes", callback_data=f"{action}:{username}" ), - types.InlineKeyboardButton( - text='No', - callback_data=f"cancel" - ) + types.InlineKeyboardButton(text="No", callback_data=f"cancel"), ) return keyboard @@ -172,10 +177,7 @@ class BotKeyboard: def inline_cancel_action(callback_data: str = "cancel"): keyboard = types.InlineKeyboardMarkup() keyboard.add( - types.InlineKeyboardButton( - text="🔙 Cancel", - callback_data=callback_data - ) + types.InlineKeyboardButton(text="🔙 Cancel", callback_data=callback_data) ) return keyboard @@ -184,107 +186,100 @@ class BotKeyboard: keyboard = types.InlineKeyboardMarkup() if len(users) >= 2: users = [p for p in users] - users = [users[i:i + 2] for i in range(0, len(users), 2)] + users = [users[i : i + 2] for i in range(0, len(users), 2)] else: users = [users] for user in users: row = [] for p in user: status = { - 'active': '✅', - 'expired': '🕰', - 'limited': 'đŸ“ĩ', - 'disabled': '❌' + "active": "✅", + "expired": "🕰", + "limited": "đŸ“ĩ", + "disabled": "❌", } - row.append(types.InlineKeyboardButton( - text=f"{p.username} ({status[p.status]})", - callback_data=f'user:{p.username}:{page}' - )) + row.append( + types.InlineKeyboardButton( + text=f"{p.username} ({status[p.status]})", + callback_data=f"user:{p.username}:{page}", + ) + ) keyboard.row(*row) # if there is more than one page if total_pages > 1: if page > 1: keyboard.add( types.InlineKeyboardButton( - text="âŦ…ī¸ Previous", - callback_data=f'users:{page - 1}' + text="âŦ…ī¸ Previous", callback_data=f"users:{page - 1}" ) ) if page < total_pages: keyboard.add( types.InlineKeyboardButton( - text="âžĄī¸ Next", - callback_data=f'users:{page + 1}' + text="âžĄī¸ Next", callback_data=f"users:{page + 1}" ) ) - keyboard.add( - types.InlineKeyboardButton( - text='🔙 Back', - callback_data='cancel' - ) - ) + keyboard.add(types.InlineKeyboardButton(text="🔙 Back", callback_data="cancel")) return keyboard @staticmethod - def select_services(services, - selected_services: List[int], - action: Literal["edit", "create"], - username: str = None, - data_limit: float = None, - expire_date: dt = None): - + def select_services( + services, + selected_services: List[int], + action: Literal["edit", "create"], + username: str = None, + data_limit: float = None, + expire_date: dt = None, + ): + keyboard = types.InlineKeyboardMarkup() if action == "edit": keyboard.add( types.InlineKeyboardButton( - text="âš ī¸ Data Limit:", - callback_data=f"help_edit" + text="âš ī¸ Data Limit:", callback_data=f"help_edit" ) ) keyboard.add( types.InlineKeyboardButton( text=f"{readable_size(data_limit) if data_limit else 'Unlimited'}", - callback_data=f"help_edit" + callback_data=f"help_edit", ), types.InlineKeyboardButton( - text="âœī¸ Edit", - callback_data=f"edit_user:{username}:data" - ) + text="âœī¸ Edit", callback_data=f"edit_user:{username}:data" + ), ) keyboard.add( types.InlineKeyboardButton( - text="📅 Expire Date:", - callback_data=f"help_edit" + text="📅 Expire Date:", callback_data=f"help_edit" ) ) keyboard.add( types.InlineKeyboardButton( text=f"{expire_date.strftime('%Y-%m-%d') if expire_date else 'Never'}", - callback_data=f"help_edit" + callback_data=f"help_edit", ), types.InlineKeyboardButton( - text="âœī¸ Edit", - callback_data=f"edit_user:{username}:expire" - ) + text="âœī¸ Edit", callback_data=f"edit_user:{username}:expire" + ), ) for s in services: keyboard.add( - types.InlineKeyboardButton( - text=f"🌐 {s.name} {'✅' if s.id in selected_services else '❌'}", - callback_data=f"switch_service:{s.id}:{action}" - ) - ) + types.InlineKeyboardButton( + text=f"🌐 {s.name} {'✅' if s.id in selected_services else '❌'}", + callback_data=f"switch_service:{s.id}:{action}", + ) + ) keyboard.add( types.InlineKeyboardButton( - text='Done', - callback_data='edit_user_done' if action == "edit" else 'add_user_done' + text="Done", + callback_data="edit_user_done" if action == "edit" else "add_user_done", ) ) keyboard.add( types.InlineKeyboardButton( - text='Cancel', - callback_data=f'user:{username}' if action == "edit" else 'cancel' + text="Cancel", + callback_data=f"user:{username}" if action == "edit" else "cancel", ) ) diff --git a/app/templates/__init__.py b/app/templates/__init__.py index 4916429..6e897c6 100644 --- a/app/templates/__init__.py +++ b/app/templates/__init__.py @@ -13,7 +13,7 @@ if CUSTOM_TEMPLATES_DIRECTORY: env = jinja2.Environment(loader=jinja2.FileSystemLoader(template_directories)) env.filters.update(CUSTOM_FILTERS) -env.globals['now'] = datetime.utcnow +env.globals["now"] = datetime.utcnow def render_template(template: str, context: Union[dict, None] = None) -> str: diff --git a/app/templates/filters.py b/app/templates/filters.py index 70af740..b9b01da 100644 --- a/app/templates/filters.py +++ b/app/templates/filters.py @@ -24,7 +24,7 @@ def only_keys(obj, *target_keys): def datetimeformat(dt): if isinstance(dt, int): dt = datetime.fromtimestamp(dt) - formatted_datetime = dt.strftime('%Y-%m-%d %H:%M:%S') + formatted_datetime = dt.strftime("%Y-%m-%d %H:%M:%S") return formatted_datetime @@ -37,5 +37,5 @@ CUSTOM_FILTERS = { "except": exclude_keys, "only": only_keys, "datetime": datetimeformat, - "bytesformat": readable_size + "bytesformat": readable_size, } diff --git a/app/utils/crypto.py b/app/utils/crypto.py index e008074..779e8e7 100644 --- a/app/utils/crypto.py +++ b/app/utils/crypto.py @@ -22,26 +22,25 @@ def generate_certificate(): cert = crypto.X509() cert.get_subject().CN = "Gozargah" cert.gmtime_adj_notBefore(0) - cert.gmtime_adj_notAfter(100*365*24*60*60) + cert.gmtime_adj_notAfter(100 * 365 * 24 * 60 * 60) cert.set_issuer(cert.get_subject()) cert.set_pubkey(k) - cert.sign(k, 'sha512') + cert.sign(k, "sha512") cert_pem = crypto.dump_certificate(crypto.FILETYPE_PEM, cert).decode("utf-8") key_pem = crypto.dump_privatekey(crypto.FILETYPE_PEM, k).decode("utf-8") - return { - "cert": cert_pem, - "key": key_pem - } + return {"cert": cert_pem, "key": key_pem} def create_secure_context( - client_cert: str, client_key: str, trusted: str, + client_cert: str, + client_key: str, + trusted: str, ) -> ssl.SSLContext: ctx = ssl.create_default_context(cafile=trusted) ctx.load_cert_chain(client_cert, client_key) - ctx.set_ciphers('ECDHE+AESGCM:ECDHE+CHACHA20:DHE+AESGCM:DHE+CHACHA20') - ctx.set_alpn_protocols(['h2']) + ctx.set_ciphers("ECDHE+AESGCM:ECDHE+CHACHA20:DHE+AESGCM:DHE+CHACHA20") + ctx.set_alpn_protocols(["h2"]) ctx.check_hostname = False ctx.verify_mode = ssl.CERT_NONE return ctx diff --git a/app/utils/jwt.py b/app/utils/jwt.py index f238113..0971433 100644 --- a/app/utils/jwt.py +++ b/app/utils/jwt.py @@ -15,7 +15,11 @@ def get_secret_key(): def create_admin_token(username: str, is_sudo=False) -> str: - data = {"sub": username, "access": "sudo" if is_sudo else "admin", "iat": datetime.utcnow()} + data = { + "sub": username, + "access": "sudo" if is_sudo else "admin", + "iat": datetime.utcnow(), + } if JWT_ACCESS_TOKEN_EXPIRE_MINUTES > 0: expire = datetime.utcnow() + timedelta(minutes=JWT_ACCESS_TOKEN_EXPIRE_MINUTES) data["exp"] = expire @@ -28,13 +32,17 @@ def get_admin_payload(token: str) -> Union[dict, None]: payload = jwt.decode(token, get_secret_key(), algorithms=["HS256"]) username: str = payload.get("sub") access: str = payload.get("access") - if not username or access not in ('admin', 'sudo'): + if not username or access not in ("admin", "sudo"): return try: - created_at = datetime.utcfromtimestamp(payload['iat']) + created_at = datetime.utcfromtimestamp(payload["iat"]) except KeyError: created_at = None - return {"username": username, "is_sudo": access == "sudo", "created_at": created_at} + return { + "username": username, + "is_sudo": access == "sudo", + "created_at": created_at, + } except JWTError: return diff --git a/app/utils/keygen.py b/app/utils/keygen.py index 11176a3..1b2654f 100644 --- a/app/utils/keygen.py +++ b/app/utils/keygen.py @@ -3,15 +3,21 @@ import xxhash import uuid from app.models.proxy import ProxyTypes + def gen_uuid(key: str) -> str: return str(uuid.UUID(bytes=xxhash.xxh128(key.encode()).digest())) + def gen_password(key: str) -> str: return xxhash.xxh128(key.encode()).hexdigest() + def generate_settings(key: str, protocol: ProxyTypes) -> dict: if protocol in (ProxyTypes.Trojan, ProxyTypes.Shadowsocks): - return ProxyTypes(protocol).settings_model(password=gen_password(key)).dict(no_obj=True) + return ( + ProxyTypes(protocol) + .settings_model(password=gen_password(key)) + .dict(no_obj=True) + ) elif protocol in (ProxyTypes.VMess, ProxyTypes.VLESS): return ProxyTypes(protocol).settings_model(id=gen_uuid(key)).dict(no_obj=True) - diff --git a/app/utils/report.py b/app/utils/report.py index ef05f6b..bfae50e 100644 --- a/app/utils/report.py +++ b/app/utils/report.py @@ -6,27 +6,60 @@ from app.db import Session, create_notification_reminder from app.db.models import UserStatus from app.models.admin import Admin from app.models.user import ReminderType, UserResponse -from app.utils.notification import (Notification, ReachedDaysLeft, - ReachedUsagePercent, UserCreated, - UserDataUsageReset, UserDeleted, - UserDisabled, UserEnabled, UserExpired, - UserLimited, UserSubscriptionRevoked, - UserUpdated, notify) - - -async def status_change(username: str, status: UserStatus, user: UserResponse, by: Optional[Admin] = None) -> None: +from app.utils.notification import ( + Notification, + ReachedDaysLeft, + ReachedUsagePercent, + UserCreated, + UserDataUsageReset, + UserDeleted, + UserDisabled, + UserEnabled, + UserExpired, + UserLimited, + UserSubscriptionRevoked, + UserUpdated, + notify, +) + + +async def status_change( + username: str, status: UserStatus, user: UserResponse, by: Optional[Admin] = None +) -> None: try: await telegram.report_status_change(username, status) except Exception: pass if status == UserStatus.limited: - await notify(UserLimited(username=username, action=Notification.Type.user_limited, user=user)) + await notify( + UserLimited( + username=username, action=Notification.Type.user_limited, user=user + ) + ) elif status == UserStatus.expired: - await notify(UserExpired(username=username, action=Notification.Type.user_expired, user=user)) + await notify( + UserExpired( + username=username, action=Notification.Type.user_expired, user=user + ) + ) elif status == UserStatus.disabled: - await notify(UserDisabled(username=username, action=Notification.Type.user_disabled, user=user, by=by)) + await notify( + UserDisabled( + username=username, + action=Notification.Type.user_disabled, + user=user, + by=by, + ) + ) elif status == UserStatus.active: - await notify(UserEnabled(username=username, action=Notification.Type.user_enabled, user=user, by=by)) + await notify( + UserEnabled( + username=username, + action=Notification.Type.user_enabled, + user=user, + by=by, + ) + ) async def user_created(user: UserResponse, user_id: int, by: Admin) -> None: @@ -37,10 +70,17 @@ async def user_created(user: UserResponse, user_id: int, by: Admin) -> None: expire_date=user.expire, data_limit=user.data_limit, # proxies=[] - services=user.services + services=user.services, + ) + + await notify( + UserCreated( + username=user.username, + action=Notification.Type.user_created, + by=by, + user=user, + ) ) - - await notify(UserCreated(username=user.username, action=Notification.Type.user_created, by=by, user=user)) async def user_updated(user: UserResponse, by: Admin) -> None: @@ -54,7 +94,14 @@ async def user_updated(user: UserResponse, by: Admin) -> None: ) except Exception: pass - await notify(UserUpdated(username=user.username, action=Notification.Type.user_updated, by=by, user=user)) + await notify( + UserUpdated( + username=user.username, + action=Notification.Type.user_updated, + by=by, + user=user, + ) + ) async def user_deleted(username: str, by: Admin) -> None: @@ -62,7 +109,9 @@ async def user_deleted(username: str, by: Admin) -> None: await telegram.report_user_deletion(username=username, by=by.username) except Exception: pass - await notify(UserDeleted(username=username, action=Notification.Type.user_deleted, by=by)) + await notify( + UserDeleted(username=username, action=Notification.Type.user_deleted, by=by) + ) async def user_data_usage_reset(user: UserResponse, by: Admin) -> None: @@ -73,7 +122,14 @@ async def user_data_usage_reset(user: UserResponse, by: Admin) -> None: ) except Exception: pass - await notify(UserDataUsageReset(username=user.username, action=Notification.Type.data_usage_reset, by=by, user=user)) + await notify( + UserDataUsageReset( + username=user.username, + action=Notification.Type.data_usage_reset, + by=by, + user=user, + ) + ) async def user_subscription_revoked(user: UserResponse, by: Admin) -> None: @@ -84,18 +140,41 @@ async def user_subscription_revoked(user: UserResponse, by: Admin) -> None: ) except Exception: pass - await notify(UserSubscriptionRevoked(username=user.username, action=Notification.Type.subscription_revoked, by=by, user=user)) + await notify( + UserSubscriptionRevoked( + username=user.username, + action=Notification.Type.subscription_revoked, + by=by, + user=user, + ) + ) async def data_usage_percent_reached( - db: Session, percent: float, user: UserResponse, user_id: int, expire: Optional[int] = None) -> None: - await notify(ReachedUsagePercent(username=user.username, user=user, used_percent=percent)) - await create_notification_reminder(db, ReminderType.data_usage, - expires_at=dt.utcfromtimestamp(expire) if expire else None, user_id=user_id) + db: Session, + percent: float, + user: UserResponse, + user_id: int, + expire: Optional[int] = None, +) -> None: + await notify( + ReachedUsagePercent(username=user.username, user=user, used_percent=percent) + ) + await create_notification_reminder( + db, + ReminderType.data_usage, + expires_at=dt.utcfromtimestamp(expire) if expire else None, + user_id=user_id, + ) -async def expire_days_reached(db: Session, days: int, user: UserResponse, user_id: int, expire: int) -> None: +async def expire_days_reached( + db: Session, days: int, user: UserResponse, user_id: int, expire: int +) -> None: await notify(ReachedDaysLeft(username=user.username, user=user, days_left=days)) await create_notification_reminder( - db, ReminderType.expiration_date, expires_at=dt.utcfromtimestamp(expire), - user_id=user_id) + db, + ReminderType.expiration_date, + expires_at=dt.utcfromtimestamp(expire), + user_id=user_id, + ) diff --git a/app/utils/share.py b/app/utils/share.py index ef4049d..3290a77 100644 --- a/app/utils/share.py +++ b/app/utils/share.py @@ -10,7 +10,12 @@ from uuid import UUID import yaml from jdatetime import date as jd -from app.models.proxy import FormatVariables, InboundBase, InboundHost, InboundHostSecurity +from app.models.proxy import ( + FormatVariables, + InboundBase, + InboundHost, + InboundHostSecurity, +) from app.templates import render_template from app.utils.system import get_public_ip, readable_size from app.utils.keygen import generate_settings @@ -31,6 +36,7 @@ STATUS_EMOJIS = { "connect_to_start": "🔌", } + class V2rayShareLink(str): @classmethod def vmess( @@ -108,8 +114,7 @@ class V2rayShareLink(str): spx="", ais="", ): - payload = {"security": tls, "type": net, - "host": host, "headerType": type} + payload = {"security": tls, "type": net, "host": host, "headerType": type} if flow and (net in ("tcp", "kcp") and type != "http"): payload["flow"] = flow @@ -159,8 +164,7 @@ class V2rayShareLink(str): spx="", ais="", ): - payload = {"security": tls, "type": net, - "host": host, "headerType": type} + payload = {"security": tls, "type": net, "host": host, "headerType": type} if flow and (net in ("tcp", "kcp") and type != "http"): payload["flow"] = flow @@ -374,7 +378,7 @@ class ClashMetaConfiguration(ClashConfiguration): sni=sni, host=host, path=path, - #headers="", + # headers="", udp=udp, alpn=alpn, ais=ais, @@ -755,7 +759,7 @@ def generate_v2ray_links(inbounds: list, key: str, extra_data: dict) -> list: def generate_clash_subscription( - inbounds: dict, key: str, extra_data: dict, is_meta: bool = False + inbounds: dict, key: str, extra_data: dict, is_meta: bool = False ) -> str: if is_meta is True: conf = ClashMetaConfiguration() @@ -768,9 +772,7 @@ def generate_clash_subscription( ) -def generate_singbox_subscription( - inbounds: dict, key: str, extra_data: dict -) -> str: +def generate_singbox_subscription(inbounds: dict, key: str, extra_data: dict) -> str: conf = SingBoxConfiguration() format_variables = setup_format_variables(extra_data) @@ -783,9 +785,7 @@ def generate_v2ray_subscription(links: list) -> str: return base64.b64encode("\n".join(links).encode()).decode() -def generate_outline_subscription( - inbounds: dict, key: str, extra_data: dict -) -> str: +def generate_outline_subscription(inbounds: dict, key: str, extra_data: dict) -> str: conf = OutlineConfiguration() format_variables = setup_format_variables(extra_data) @@ -847,10 +847,11 @@ def format_time_left(seconds_left: int) -> str: result.append(f"{seconds}s") return " ".join(result) + from app.models.user import UserStatus + def setup_format_variables(extra_data: dict) -> dict: - user_status = extra_data.get("status") expire_timestamp = extra_data.get("expire") @@ -920,9 +921,14 @@ def process_inbounds_and_tags( ) -> Union[List, str]: salt = secrets.token_hex(8) results = [] - + for inb in inbounds: - inb_id, inbound, protocol, tag = inb.id, json.loads(inb.config), inb.protocol, inb.tag + inb_id, inbound, protocol, tag = ( + inb.id, + json.loads(inb.config), + inb.protocol, + inb.tag, + ) settings = generate_settings(ukey, protocol) format_variables.update({"PROTOCOL": protocol.name}) @@ -948,8 +954,12 @@ def process_inbounds_and_tags( req_host = random.choice(req_host_list).replace("*", salt) else: req_host = "" - - host_tls = None if host.security == InboundHostSecurity.inbound_default else host.security.value + + host_tls = ( + None + if host.security == InboundHostSecurity.inbound_default + else host.security.value + ) host_inbound.update( { "port": host.port or inbound["port"], @@ -957,11 +967,13 @@ def process_inbounds_and_tags( "host": req_host, "tls": inbound["tls"] if host_tls is None else host_tls, "alpn": host.alpn.value or inbound.get("alpn", ""), - "path": inbound.get("path", "") - if host.path is None - else host.path.format_map(format_variables), + "path": ( + inbound.get("path", "") + if host.path is None + else host.path.format_map(format_variables) + ), "fp": host.fingerprint.value or inbound.get("fp", ""), - "ais": host.allowinsecure or "" + "ais": host.allowinsecure or "", } ) @@ -969,8 +981,7 @@ def process_inbounds_and_tags( results.append( get_v2ray_link( remark=host.remark.format_map(format_variables), - address=host.address.format_map( - format_variables), + address=host.address.format_map(format_variables), inbound=host_inbound, settings=settings, ) diff --git a/app/utils/system.py b/app/utils/system.py index 0efa01e..0e97f0d 100644 --- a/app/utils/system.py +++ b/app/utils/system.py @@ -57,13 +57,17 @@ class RealtimeBandwidthStat: rt_bw = RealtimeBandwidth( - incoming_bytes=0, outgoing_bytes=0, incoming_packets=0, outgoing_packets=0) + incoming_bytes=0, outgoing_bytes=0, incoming_packets=0, outgoing_packets=0 +) def realtime_bandwidth() -> RealtimeBandwidthStat: return RealtimeBandwidthStat( - incoming_bytes=rt_bw.incoming_bytes, outgoing_bytes=rt_bw.outgoing_bytes, - incoming_packets=rt_bw.incoming_packets, outgoing_packets=rt_bw.outgoing_packets) + incoming_bytes=rt_bw.incoming_bytes, + outgoing_bytes=rt_bw.outgoing_bytes, + incoming_packets=rt_bw.incoming_packets, + outgoing_packets=rt_bw.outgoing_packets, + ) def random_password() -> str: @@ -73,7 +77,7 @@ def random_password() -> str: def check_port(port: int) -> bool: s = socket.socket() try: - s.connect(('127.0.0.1', port)) + s.connect(("127.0.0.1", port)) return True except socket.error: return False @@ -84,14 +88,14 @@ def check_port(port: int) -> bool: def get_public_ip(): try: sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) - sock.connect(('8.8.8.8', 80)) + sock.connect(("8.8.8.8", 80)) return sock.getsockname()[0] except (socket.error, IndexError): pass finally: sock.close() - return '127.0.0.1' + return "127.0.0.1" def readable_size(size_bytes): @@ -101,4 +105,4 @@ def readable_size(size_bytes): i = int(math.floor(math.log(size_bytes, 1024))) p = math.pow(1024, i) s = round(size_bytes / p, 2) - return f'{s} {size_name[i]}' + return f"{s} {size_name[i]}" diff --git a/cli/admin.py b/cli/admin.py index 1a3034f..492fbd3 100644 --- a/cli/admin.py +++ b/cli/admin.py @@ -20,26 +20,34 @@ app = typer.Typer(no_args_is_help=True) def list_admins( offset: Optional[int] = typer.Option(None, *utils.FLAGS["offset"]), limit: Optional[int] = typer.Option(None, *utils.FLAGS["limit"]), - username: Optional[str] = typer.Option(None, *utils.FLAGS["username"], help="Search by username"), + username: Optional[str] = typer.Option( + None, *utils.FLAGS["username"], help="Search by username" + ), ): """Displays a table of admins""" with GetDB() as db: - admins: list[Admin] = crud.get_admins(db, offset=offset, limit=limit, username=username) + admins: list[Admin] = crud.get_admins( + db, offset=offset, limit=limit, username=username + ) utils.print_table( table=Table("Username", "Is sudo", "Created at"), rows=[ - (str(admin.username), - "âœ”ī¸" if admin.is_sudo else "âœ–ī¸", - utils.readable_datetime(admin.created_at)) + ( + str(admin.username), + "âœ”ī¸" if admin.is_sudo else "âœ–ī¸", + utils.readable_datetime(admin.created_at), + ) for admin in admins - ] + ], ) @app.command(name="delete") def delete_admin( username: str = typer.Option(..., *utils.FLAGS["username"], prompt=True), - yes_to_all: bool = typer.Option(False, *utils.FLAGS["yes_to_all"], help="Skips confirmations") + yes_to_all: bool = typer.Option( + False, *utils.FLAGS["yes_to_all"], help="Skips confirmations" + ), ): """ Deletes the specified admin @@ -49,9 +57,11 @@ def delete_admin( with GetDB() as db: admin: Union[Admin, None] = crud.get_admin(db, username=username) if not admin: - utils.error(f"There's no admin with username \"{username}\"!") + utils.error(f'There\'s no admin with username "{username}"!') - if yes_to_all or typer.confirm(f'Are you sure about deleting "{username}"?', default=False): + if yes_to_all or typer.confirm( + f'Are you sure about deleting "{username}"?', default=False + ): crud.remove_admin(db, admin) utils.success(f'"{username}" deleted successfully.') else: @@ -62,8 +72,14 @@ def delete_admin( def create_admin( username: str = typer.Option(..., *utils.FLAGS["username"], prompt=True), is_sudo: bool = typer.Option(None, *utils.FLAGS["is_sudo"], prompt=True), - password: str = typer.Option(..., prompt=True, confirmation_prompt=True, - hide_input=True, hidden=True, envvar=utils.PASSWORD_ENVIRON_NAME) + password: str = typer.Option( + ..., + prompt=True, + confirmation_prompt=True, + hide_input=True, + hidden=True, + envvar=utils.PASSWORD_ENVIRON_NAME, + ), ): """ Creates an admin @@ -72,14 +88,18 @@ def create_admin( """ with GetDB() as db: try: - crud.create_admin(db, AdminCreate(username=username, password=password, is_sudo=is_sudo)) + crud.create_admin( + db, AdminCreate(username=username, password=password, is_sudo=is_sudo) + ) utils.success(f'Admin "{username}" created successfully.') except IntegrityError: utils.error(f'Admin "{username}" already exists!') @app.command(name="update") -def update_admin(username: str = typer.Option(..., *utils.FLAGS["username"], prompt=True)): +def update_admin( + username: str = typer.Option(..., *utils.FLAGS["username"], prompt=True) +): """ Updates the specified admin @@ -88,17 +108,22 @@ def update_admin(username: str = typer.Option(..., *utils.FLAGS["username"], pro def _get_modify_model(admin: Admin): Console().print( - Panel(f'Editing "{username}". Just press "Enter" to leave each field unchanged.') + Panel( + f'Editing "{username}". Just press "Enter" to leave each field unchanged.' + ) ) is_sudo: bool = typer.confirm("Is sudo", default=admin.is_sudo) - new_password: Union[str, None] = typer.prompt( - "New password", - default="", - show_default=False, - confirmation_prompt=True, - hide_input=True - ) or None + new_password: Union[str, None] = ( + typer.prompt( + "New password", + default="", + show_default=False, + confirmation_prompt=True, + hide_input=True, + ) + or None + ) return AdminPartialModify( is_sudo=is_sudo, @@ -108,14 +133,18 @@ def update_admin(username: str = typer.Option(..., *utils.FLAGS["username"], pro with GetDB() as db: admin: Union[Admin, None] = crud.get_admin(db, username=username) if not admin: - utils.error(f"There's no admin with username \"{username}\"!") + utils.error(f'There\'s no admin with username "{username}"!') crud.partial_update_admin(db, admin, _get_modify_model(admin)) utils.success(f'Admin "{username}" updated successfully.') @app.command(name="import-from-env") -def import_from_env(yes_to_all: bool = typer.Option(False, *utils.FLAGS["yes_to_all"], help="Skips confirmations")): +def import_from_env( + yes_to_all: bool = typer.Option( + False, *utils.FLAGS["yes_to_all"], help="Skips confirmations" + ) +): """ Imports the sudo admin from env @@ -134,8 +163,10 @@ def import_from_env(yes_to_all: bool = typer.Option(False, *utils.FLAGS["yes_to_ ) if not (username and password): - utils.error("Unable to retrieve username and password.\n" - "Make sure both SUDO_USERNAME and SUDO_PASSWORD are set.") + utils.error( + "Unable to retrieve username and password.\n" + "Make sure both SUDO_USERNAME and SUDO_PASSWORD are set." + ) with GetDB() as db: admin: Union[None, Admin] = None @@ -143,28 +174,27 @@ def import_from_env(yes_to_all: bool = typer.Option(False, *utils.FLAGS["yes_to_ # If env admin already exists if current_admin := crud.get_admin(db, username=username): if not yes_to_all and not typer.confirm( - f'Admin "{username}" already exists. Do you want to sync it with env?', default=None + f'Admin "{username}" already exists. Do you want to sync it with env?', + default=None, ): utils.error("Aborted.") admin = crud.partial_update_admin( - db, - current_admin, - AdminPartialModify(password=password, is_sudo=True) + db, current_admin, AdminPartialModify(password=password, is_sudo=True) ) # If env admin does not exist yet else: - admin = crud.create_admin(db, AdminCreate( - username=username, - password=password, - is_sudo=True - )) + admin = crud.create_admin( + db, AdminCreate(username=username, password=password, is_sudo=True) + ) - updated_user_count = db.query(User).filter_by(admin_id=None).update({"admin_id": admin.id}) + updated_user_count = ( + db.query(User).filter_by(admin_id=None).update({"admin_id": admin.id}) + ) db.commit() utils.success( f'Admin "{username}" imported successfully.\n' f"{updated_user_count} users' admin_id set to the {username}'s id.\n" - 'You must delete SUDO_USERNAME and SUDO_PASSWORD from your env file now.' + "You must delete SUDO_USERNAME and SUDO_PASSWORD from your env file now." ) diff --git a/cli/subscription.py b/cli/subscription.py index fa0ea97..4b27c62 100644 --- a/cli/subscription.py +++ b/cli/subscription.py @@ -19,9 +19,7 @@ class ConfigFormat(str, Enum): @app.command(name="get-link") -def get_link( - username: str = typer.Option(..., *utils.FLAGS["username"], prompt=True) -): +def get_link(username: str = typer.Option(..., *utils.FLAGS["username"], prompt=True)): """ Prints the given user's subscription link. @@ -36,13 +34,20 @@ def get_link( @app.command(name="get-config") def get_config( username: str = typer.Option(..., *utils.FLAGS["username"], prompt=True), - config_format: ConfigFormat = typer.Option(..., *utils.FLAGS["format"], prompt=True), + config_format: ConfigFormat = typer.Option( + ..., *utils.FLAGS["format"], prompt=True + ), output_file: Optional[str] = typer.Option( - None, *utils.FLAGS["output_file"], help="Writes the generated config in the file if provided" + None, + *utils.FLAGS["output_file"], + help="Writes the generated config in the file if provided", ), as_base64: bool = typer.Option( - False, "--base64", is_flag=True, help="Encodes output in base64 format if present" - ) + False, + "--base64", + is_flag=True, + help="Encodes output in base64 format if present", + ), ): """ Generates a subscription config. @@ -68,8 +73,8 @@ def get_config( ) else: utils.success( - 'No output file specified.' + "No output file specified." f' using pager for {username}\'s config in "{config_format}" format.', - auto_exit=False + auto_exit=False, ) utils.paginate(conf) diff --git a/cli/user.py b/cli/user.py index 52c6820..f7bd605 100644 --- a/cli/user.py +++ b/cli/user.py @@ -16,9 +16,13 @@ app = typer.Typer(no_args_is_help=True) def list_users( offset: Optional[int] = typer.Option(None, *utils.FLAGS["offset"]), limit: Optional[int] = typer.Option(None, *utils.FLAGS["limit"]), - username: Optional[List[str]] = typer.Option(None, *utils.FLAGS["username"], help="Search by username(s)"), + username: Optional[List[str]] = typer.Option( + None, *utils.FLAGS["username"], help="Search by username(s)" + ), status: Optional[crud.UserStatus] = typer.Option(None, *utils.FLAGS["status"]), - admin: Optional[str] = typer.Option(None, "--admin", "--owner", help="Search by owner admin's username") + admin: Optional[str] = typer.Option( + None, "--admin", "--owner", help="Search by owner admin's username" + ), ): """ Displays a table of users @@ -27,15 +31,24 @@ def list_users( """ with GetDB() as db: users: list[User] = crud.get_users( - db=db, offset=offset, limit=limit, - usernames=username, status=status, - admin=crud.get_admin(db, admin) if admin else None + db=db, + offset=offset, + limit=limit, + usernames=username, + status=status, + admin=crud.get_admin(db, admin) if admin else None, ) utils.print_table( table=Table( - "ID", "Username", "Status", "Used traffic", - "Data limit", "Reset strategy", "Expires at", "Owner", + "ID", + "Username", + "Status", + "Used traffic", + "Data limit", + "Reset strategy", + "Expires at", + "Owner", ), rows=[ ( @@ -46,18 +59,22 @@ def list_users( readable_size(user.data_limit) if user.data_limit else "Unlimited", user.data_limit_reset_strategy.value, utils.readable_datetime(user.expire, include_time=False), - user.admin.username if user.admin else '' + user.admin.username if user.admin else "", ) for user in users - ] + ], ) @app.command(name="set-owner") def set_owner( username: str = typer.Option(None, *utils.FLAGS["username"], prompt=True), - admin: str = typer.Option(None, "--admin", "--owner", prompt=True, help="Admin's username"), - yes_to_all: bool = typer.Option(False, *utils.FLAGS["yes_to_all"], help="Skips confirmations") + admin: str = typer.Option( + None, "--admin", "--owner", prompt=True, help="Admin's username" + ), + yes_to_all: bool = typer.Option( + False, *utils.FLAGS["yes_to_all"], help="Skips confirmations" + ), ): """ Transfers user's ownership @@ -66,15 +83,21 @@ def set_owner( """ with GetDB() as db: user: User = utils.raise_if_falsy( - crud.get_user(db, username=username), f'User "{username}" not found.') + crud.get_user(db, username=username), f'User "{username}" not found.' + ) dbadmin = utils.raise_if_falsy( - crud.get_admin(db, username=admin), f'Admin "{admin}" not found.') + crud.get_admin(db, username=admin), f'Admin "{admin}" not found.' + ) # Ask for confirmation if user already has an owner - if user.admin and not yes_to_all and not typer.confirm( - f'{username}\'s current owner is "{user.admin.username}".' - f' Are you sure about transferring its ownership to "{admin}"?' + if ( + user.admin + and not yes_to_all + and not typer.confirm( + f'{username}\'s current owner is "{user.admin.username}".' + f' Are you sure about transferring its ownership to "{admin}"?' + ) ): utils.error("Aborted.") diff --git a/cli/utils.py b/cli/utils.py index 0ff37d1..0f563c0 100644 --- a/cli/utils.py +++ b/cli/utils.py @@ -51,9 +51,7 @@ def get_user(db, username: str) -> User: def print_table( - table: Table, - rows: Iterable[Iterable[Any]], - console: Optional[Console] = None + table: Table, rows: Iterable[Iterable[Any]], console: Optional[Console] = None ): for row in rows: table.add_row(*row) @@ -64,7 +62,7 @@ def print_table( def readable_datetime( date_time: Union[datetime, int, None], include_date: bool = True, - include_time: bool = True + include_time: bool = True, ): def get_datetime_format(): dt_format = "" diff --git a/config.py b/config.py index 695b9e3..328b55e 100755 --- a/config.py +++ b/config.py @@ -5,7 +5,9 @@ from dotenv import load_dotenv load_dotenv() -SQLALCHEMY_DATABASE_URL = config("SQLALCHEMY_DATABASE_URL", default="sqlite:///db.sqlite3") +SQLALCHEMY_DATABASE_URL = config( + "SQLALCHEMY_DATABASE_URL", default="sqlite:///db.sqlite3" +) UVICORN_HOST = config("UVICORN_HOST", default="0.0.0.0") @@ -18,42 +20,55 @@ UVICORN_SSL_KEYFILE = config("UVICORN_SSL_KEYFILE", default=None) DEBUG = config("DEBUG", default=False, cast=bool) DOCS = config("DOCS", default=False, cast=bool) -VITE_BASE_API = f"http://127.0.0.1:{UVICORN_PORT}/api/" \ - if DEBUG and config("VITE_BASE_API", default="/api/") == "/api/" \ +VITE_BASE_API = ( + f"http://127.0.0.1:{UVICORN_PORT}/api/" + if DEBUG and config("VITE_BASE_API", default="/api/") == "/api/" else config("VITE_BASE_API", default="/api/") +) -XRAY_FALLBACKS_INBOUND_TAG = config("XRAY_FALLBACKS_INBOUND_TAG", cast=str, default="") or config( - "XRAY_FALLBACK_INBOUND_TAG", cast=str, default="" +XRAY_FALLBACKS_INBOUND_TAG = config( + "XRAY_FALLBACKS_INBOUND_TAG", cast=str, default="" +) or config("XRAY_FALLBACK_INBOUND_TAG", cast=str, default="") +XRAY_SUBSCRIPTION_URL_PREFIX = config("XRAY_SUBSCRIPTION_URL_PREFIX", default="").strip( + "/" ) -XRAY_SUBSCRIPTION_URL_PREFIX = config("XRAY_SUBSCRIPTION_URL_PREFIX", default="").strip("/") TELEGRAM_API_TOKEN = config("TELEGRAM_API_TOKEN", default="") TELEGRAM_ADMIN_ID = config( - 'TELEGRAM_ADMIN_ID', + "TELEGRAM_ADMIN_ID", default="", - cast=lambda v: [int(i) for i in filter(str.isdigit, (s.strip() for s in v.split(',')))] + cast=lambda v: [ + int(i) for i in filter(str.isdigit, (s.strip() for s in v.split(","))) + ], ) TELEGRAM_PROXY_URL = config("TELEGRAM_PROXY_URL", default="") TELEGRAM_LOGGER_CHANNEL_ID = config("TELEGRAM_LOGGER_CHANNEL_ID", cast=int, default=0) TELEGRAM_DEFAULT_VLESS_FLOW = config("TELEGRAM_DEFAULT_VLESS_FLOW", default="") -JWT_ACCESS_TOKEN_EXPIRE_MINUTES = config("JWT_ACCESS_TOKEN_EXPIRE_MINUTES", cast=int, default=1440) +JWT_ACCESS_TOKEN_EXPIRE_MINUTES = config( + "JWT_ACCESS_TOKEN_EXPIRE_MINUTES", cast=int, default=1440 +) CUSTOM_TEMPLATES_DIRECTORY = config("CUSTOM_TEMPLATES_DIRECTORY", default=None) -CLASH_SUBSCRIPTION_TEMPLATE = config("CLASH_SUBSCRIPTION_TEMPLATE", default="clash/default.yml") -SUBSCRIPTION_PAGE_TEMPLATE = config("SUBSCRIPTION_PAGE_TEMPLATE", default="subscription/index.html") +CLASH_SUBSCRIPTION_TEMPLATE = config( + "CLASH_SUBSCRIPTION_TEMPLATE", default="clash/default.yml" +) +SUBSCRIPTION_PAGE_TEMPLATE = config( + "SUBSCRIPTION_PAGE_TEMPLATE", default="subscription/index.html" +) HOME_PAGE_TEMPLATE = config("HOME_PAGE_TEMPLATE", default="home/index.html") SINGBOX_SUBSCRIPTION_TEMPLATE = config( - "SINGBOX_SUBSCRIPTION_TEMPLATE", - default="singbox/default.json" + "SINGBOX_SUBSCRIPTION_TEMPLATE", default="singbox/default.json" ) # USERNAME: PASSWORD -SUDOERS = {config("SUDO_USERNAME"): config("SUDO_PASSWORD")} \ - if config("SUDO_USERNAME", default='') and config("SUDO_PASSWORD", default='') \ +SUDOERS = ( + {config("SUDO_USERNAME"): config("SUDO_PASSWORD")} + if config("SUDO_USERNAME", default="") and config("SUDO_PASSWORD", default="") else {} +) WEBHOOK_ADDRESS = config("WEBHOOK_ADDRESS", default=None) @@ -62,19 +77,27 @@ WEBHOOK_SECRET = config("WEBHOOK_SECRET", default=None) # recurrent notifications # timeout between each retry of sending a notification in seconds -RECURRENT_NOTIFICATIONS_TIMEOUT = config("RECURRENT_NOTIFICATIONS_TIMEOUT", default=180, cast=int) +RECURRENT_NOTIFICATIONS_TIMEOUT = config( + "RECURRENT_NOTIFICATIONS_TIMEOUT", default=180, cast=int +) # how many times to try after ok response not recevied after sending a notifications -NUMBER_OF_RECURRENT_NOTIFICATIONS = config("NUMBER_OF_RECURRENT_NOTIFICATIONS", default=3, cast=int) +NUMBER_OF_RECURRENT_NOTIFICATIONS = config( + "NUMBER_OF_RECURRENT_NOTIFICATIONS", default=3, cast=int +) # sends a notification when the user uses this much of thier data -NOTIFY_REACHED_USAGE_PERCENT = config("NOTIFY_REACHED_USAGE_PERCENT", default=80, cast=int) +NOTIFY_REACHED_USAGE_PERCENT = config( + "NOTIFY_REACHED_USAGE_PERCENT", default=80, cast=int +) # sends a notification when there is n days left of their service NOTIFY_DAYS_LEFT = config("NOTIFY_DAYS_LEFT", default=3, cast=int) -DISABLE_RECORDING_NODE_USAGE = config("DISABLE_RECORDING_NODE_USAGE", cast=bool, default=False) +DISABLE_RECORDING_NODE_USAGE = config( + "DISABLE_RECORDING_NODE_USAGE", cast=bool, default=False +) # headers: profile-update-interval, support-url, profile-title SUB_UPDATE_INTERVAL = config("SUB_UPDATE_INTERVAL", default="12") SUB_SUPPORT_URL = config("SUB_SUPPORT_URL", default="https://t.me/") -SUB_PROFILE_TITLE = config("SUB_PROFILE_TITLE", default="Subscription") \ No newline at end of file +SUB_PROFILE_TITLE = config("SUB_PROFILE_TITLE", default="Subscription")