diff --git 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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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 <b>"
f"{'+' if data_limit > 0 else '-'}{readable_size(abs(data_limit *1024*1024*1024))}</b>",
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 <b>{days} Days</b>",
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 <code>{username}</code>",
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 <code>{username}</code>",
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]} <b>Status:</b> <code>{status.title()}</code>
│ └─<b>Username:</b> <code>{username}</code>
@@ -536,47 +586,54 @@ def get_user_info_text(
│
"""
if note:
- text += f'├─📝 <b>Note:</b> <code>{note}</code>\n│\n'
+ text += f"├─📝 <b>Note:</b> <code>{note}</code>\n│\n"
text += f'└─🚀 <b><a href="{sub_url}">Subscription</a>:</b> <code>{sub_url}</code>'
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'<b>📝 Current Note:</b> <code>{db_user.note}</code>\n\nSend new Note for <code>{username}</code>',
+ f"<b>📝 Current Note:</b> <code>{db_user.note}</code>\n\nSend new Note for <code>{username}</code>",
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"""\
📝 <b>#Edit_Note #From_Bot</b>
➖➖➖➖➖➖➖➖➖
<b>Username :</b> <code>{username}</code>
<b>Last Note :</b> <code>{last_note}</code>
<b>New Note :</b> <code>{note}</code>
➖➖➖➖➖➖➖➖➖
-<b>By :</b> <a href="tg://user?id={message.chat.id}">{message.from_user.full_name}</a>'''
+<b>By :</b> <a href="tg://user?id={message.chat.id}">{message.from_user.full_name}</a>"""
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"<code>{link}</code>",
- 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"""\
🗑 <b>#Deleted #From_Bot</b>
➖➖➖➖➖➖➖➖➖
<b>Username :</b> <code>{db_user.username}</code>
@@ -957,9 +1069,9 @@ async def delete_user(call: types.CallbackQuery):
<b>Expire Date :</b> <code>\
{datetime.fromtimestamp(db_user.expire).strftime('%H:%M:%S %Y-%m-%d') if db_user.expire else "Never"}</code>
➖➖➖➖➖➖➖➖➖
-<b>By :</b> <a href="tg://user?id={call.from_user.id}">{call.from_user.full_name}</a>'''
+<b>By :</b> <a href="tg://user?id={call.from_user.id}">{call.from_user.full_name}</a>"""
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"""\
❌ <b>#Disabled #From_Bot</b>
➖➖➖➖➖➖➖➖➖
<b>Username</b> : <code>{username}</code>
➖➖➖➖➖➖➖➖➖
-<b>By :</b> <a href="tg://user?id={call.from_user.id}">{call.from_user.full_name}</a>'''
+<b>By :</b> <a href="tg://user?id={call.from_user.id}">{call.from_user.full_name}</a>"""
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"""\
✅ <b>#Activated #From_Bot</b>
➖➖➖➖➖➖➖➖➖
<b>Username</b> : <code>{username}</code>
➖➖➖➖➖➖➖➖➖
-<b>By :</b> <a href="tg://user?id={call.from_user.id}">{call.from_user.full_name}</a>'''
+<b>By :</b> <a href="tg://user?id={call.from_user.id}">{call.from_user.full_name}</a>"""
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"""\
🔁 <b>#Reset_usage #From_Bot</b>
➖➖➖➖➖➖➖➖➖
<b>Username</b> : <code>{username}</code>
➖➖➖➖➖➖➖➖➖
-<b>By :</b> <a href="tg://user?id={call.from_user.id}">{call.from_user.full_name}</a>'''
+<b>By :</b> <a href="tg://user?id={call.from_user.id}">{call.from_user.full_name}</a>"""
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➖➖➖➖➖➖➖➖➖ \n<b>By :</b> <a href="tg://user?id={call.from_user.id}">{call.from_user.full_name}</a>'
if last_user.data_limit != user.data_limit:
- text = f'''\
+ text = f"""\
📶 <b>#Traffic_Change #From_Bot</b>
➖➖➖➖➖➖➖➖➖
<b>Username :</b> <code>{user.username}</code>
<b>Last Traffic Limit :</b> <code>{readable_size(last_user.data_limit) if last_user.data_limit else "Unlimited"}</code>
-<b>New Traffic Limit :</b> <code>{readable_size(user.data_limit) if user.data_limit else "Unlimited"}</code>{tag}'''
+<b>New Traffic Limit :</b> <code>{readable_size(user.data_limit) if user.data_limit else "Unlimited"}</code>{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"""\
📅 <b>#Expiry_Change #From_Bot</b>
➖➖➖➖➖➖➖➖➖
<b>Username :</b> <code>{user.username}</code>
<b>Last Expire Date :</b> <code>\
{datetime.fromtimestamp(last_user.expire).strftime('%H:%M:%S %Y-%m-%d') if last_user.expire else "Never"}</code>
<b>New Expire Date :</b> <code>\
-{datetime.fromtimestamp(user.expire).strftime('%H:%M:%S %Y-%m-%d') if user.expire else "Never"}</code>{tag}'''
+{datetime.fromtimestamp(user.expire).strftime('%H:%M:%S %Y-%m-%d') if user.expire else "Never"}</code>{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"""\
⚙️ <b>#Inbounds_Change #From_Bot</b>
➖➖➖➖➖➖➖➖➖
<b>Username :</b> <code>{user.username}</code>
<b>Last Proxies :</b> <code>{", ".join(list(last_user.inbounds.values())[0])}</code>
-<b>New Proxies :</b> <code>{", ".join(list(user.inbounds.values())[0])}</code>{tag}'''
+<b>New Proxies :</b> <code>{", ".join(list(user.inbounds.values())[0])}</code>{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"""\
🆕 <b>#Created #From_Bot</b>
➖➖➖➖➖➖➖➖➖
<b>Username :</b> <code>{user.username}</code>
@@ -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"}</code>
<b>Services :</b> <code>{"" if not services else ", ".join([s.name for s in services])}</code>
➖➖➖➖➖➖➖➖➖
-<b>By :</b> <a href="tg://user?id={call.from_user.id}">{call.from_user.full_name}</a>'''
+<b>By :</b> <a href="tg://user?id={call.from_user.id}">{call.from_user.full_name}</a>"""
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(
- '⏳ <b>In Progress...</b>',
+ "⏳ <b>In Progress...</b>",
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'✅ <code>{deleted}</code>/<code>{len(depleted_users)}</code> <b>{data[7:].title()} Users Deleted</b>',
+ f"✅ <code>{deleted}</code>/<code>{len(depleted_users)}</code> <b>{data[7:].title()} Users Deleted</b>",
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"""\
🗑 <b>#Delete #{data[7:].title()} #From_Bot</b>
➖➖➖➖➖➖➖➖➖
<b>Count:</b> <code>{deleted}</code>
➖➖➖➖➖➖➖➖➖
-<b>By :</b> <a href="tg://user?id={call.from_user.id}">{call.from_user.full_name}</a>'''
+<b>By :</b> <a href="tg://user?id={call.from_user.id}">{call.from_user.full_name}</a>"""
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, '⏳ <b>In Progress...</b>', 'HTML')).id)
+ (await bot.send_message(chat_id, "⏳ <b>In Progress...</b>", "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'✅ <b>{counter}/{len(users)} Users</b> Data Limit according to <code>{"+" if data_limit > 0 else "-"}{readable_size(abs(data_limit))}</code>',
- 'HTML',
- reply_markup=BotKeyboard.main_menu())
+ "HTML",
+ reply_markup=BotKeyboard.main_menu(),
+ )
if TELEGRAM_LOGGER_CHANNEL_ID:
- text = f'''\
+ text = f"""\
📶 <b>#Traffic_Change #From_Bot</b>
➖➖➖➖➖➖➖➖➖
<b>According to:</b> <code>{"+" if data_limit > 0 else "-"}{readable_size(abs(data_limit))}</code>
<b>Count:</b> <code>{counter}</code>
➖➖➖➖➖➖➖➖➖
-<b>By :</b> <a href="tg://user?id={call.from_user.id}">{call.from_user.full_name}</a>'''
+<b>By :</b> <a href="tg://user?id={call.from_user.id}">{call.from_user.full_name}</a>"""
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, '⏳ <b>In Progress...</b>', 'HTML')).id)
+ (await bot.send_message(chat_id, "⏳ <b>In Progress...</b>", "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'✅ <b>{counter}/{len(users)} Users</b> Expiry Changes according to {days} Days',
- 'HTML',
- reply_markup=BotKeyboard.main_menu())
+ f"✅ <b>{counter}/{len(users)} Users</b> Expiry Changes according to {days} Days",
+ "HTML",
+ reply_markup=BotKeyboard.main_menu(),
+ )
if TELEGRAM_LOGGER_CHANNEL_ID:
- text = f'''\
+ text = f"""\
📅 <b>#Expiry_Change #From_Bot</b>
➖➖➖➖➖➖➖➖➖
<b>According to:</b> <code>{days} Days</code>
<b>Count:</b> <code>{counter}</code>
➖➖➖➖➖➖➖➖➖
-<b>By :</b> <a href="tg://user?id={call.from_user.id}">{call.from_user.full_name}</a>'''
+<b>By :</b> <a href="tg://user?id={call.from_user.id}">{call.from_user.full_name}</a>"""
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"""\
🚫 <b>#Revoke_sub #From_Bot</b>
➖➖➖➖➖➖➖➖➖
<b>Username:</b> <code>{username}</code>
➖➖➖➖➖➖➖➖➖
-<b>By :</b> <a href="tg://user?id={call.from_user.id}">{call.from_user.full_name}</a>'''
+<b>By :</b> <a href="tg://user?id={call.from_user.id}">{call.from_user.full_name}</a>"""
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
@@ -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 = """\
🆕 <b>#Created</b>
➖➖➖➖➖➖➖➖➖
<b>Username :</b> <code>{username}</code>
@@ -34,26 +45,30 @@ async def report_new_user(user_id: int, username: str, by: str, expire_date: int
<b>Expire Date :</b> <code>{expire_date}</code>
<b>Services :</b> <code>{services}</code>
➖➖➖➖➖➖➖➖➖
-<b>By :</b> <b>#{by}</b>'''.format(
+<b>By :</b> <b>#{by}</b>""".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 = """\
✏️ <b>#Modified</b>
➖➖➖➖➖➖➖➖➖
<b>Username :</b> <code>{username}</code>
@@ -62,48 +77,52 @@ async def report_user_modification(username: str, expire_date: int, data_limit:
<b>Services :</b> <code>{services}</code>
➖➖➖➖➖➖➖➖➖
<b>By :</b> <b>#{by}</b>\
- '''.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 = """\
🗑 <b>#Deleted</b>
➖➖➖➖➖➖➖➖➖
<b>Username</b> : <code>{username}</code>
➖➖➖➖➖➖➖➖➖
<b>By</b> : <b>#{by}</b>\
- '''.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': '✅ <b>#Activated</b>',
- 'disabled': '❌ <b>#Disabled</b>',
- 'limited': '🪫 <b>#Limited</b>',
- 'expired': '🕔 <b>#Expired</b>'
+ "active": "✅ <b>#Activated</b>",
+ "disabled": "❌ <b>#Disabled</b>",
+ "limited": "🪫 <b>#Limited</b>",
+ "expired": "🕔 <b>#Expired</b>",
}
- text = '''\
+ text = """\
{status}
➖➖➖➖➖➖➖➖➖
<b>Username</b> : <code>{username}</code>\
- '''.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):
➖➖➖➖➖➖➖➖➖
<b>By</b> : <b>#{by}</b>\
""".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):
➖➖➖➖➖➖➖➖➖
<b>By</b> : <b>#{by}</b>\
""".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
@@ -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 <username>`', parse_mode='MarkdownV2')
+ return bot.reply_to(
+ message, "Usage: `/usage <username>`", 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]} <b>Status:</b> <code>{user.status.title()}</code>
│ └─<b>Username:</b> <code>{user.username}</code>
│
@@ -38,6 +37,6 @@ def usage_command(message):
│ └─<b>Data Used:</b> <code>{readable_size(user.used_traffic) if user.used_traffic else "-"}</code>
│
└─📅 <b>Expiry Date:</b> <code>{datetime.fromtimestamp(user.expire).date() if user.expire else 'Never'}</code>
- └─<b>Days left:</b> <code>{(datetime.fromtimestamp(user.expire or 0) - datetime.now()).days if user.expire else '-'}</code>'''
+ └─<b>Days left:</b> <code>{(datetime.fromtimestamp(user.expire or 0) - datetime.now()).days if user.expire else '-'}</code>"""
- 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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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")