gitscope
← marzneshin / Diff · f1ab16c → f1ab16c
Changes f1ab16c..f1ab16c
Patch commit
diff --git a/.github/workflows/api-ci.yml b/.github/workflows/api-ci.yml
index d13a6df..5868279 100644
--- a/.github/workflows/api-ci.yml
+++ b/.github/workflows/api-ci.yml
@@ -1,23 +1,37 @@
-name: Pylint
-
-on: [push]
+name: Lint
+on:
+ push:
+ branches: [ "master" ]
+ paths:
+ - 'app/**'
+ pull_request:
+ branches: [ "master" ]
+ paths:
+ - 'app/**'
jobs:
- build:
+ lint:
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v3
- - name: Set up Python 3.11.2
- uses: actions/setup-python@v3
- with:
- python-version: 3.11.2
+ - uses: actions/checkout@v3
+
+ - name: black
+ uses: psf/black@stable
+ with:
+ options: "--check --verbose"
+ src: "./app"
+
+ - name: Set up Python 3.11.2
+ uses: actions/setup-python@v3
+ with:
+ python-version: 3.11.2
- - name: Install dependencies
- run: |
- python -m pip install --upgrade pip
- pip install pylint
- pip install -r requirements.txt
+ - name: Install dependencies
+ run: |
+ python -m pip install --upgrade pip
+ pip install pylint
+ pip install -r requirements.txt
- - name: Lint with pylint
- run: |
+ - name: Lint with pylint
+ run: |
pylint --exit-zero "$(git ls-files '*.py')"
diff --git a/.github/workflows/api-codeql.yml b/.github/workflows/api-codeql.yml
index da19181..a20c6a5 100644
--- a/.github/workflows/api-codeql.yml
+++ b/.github/workflows/api-codeql.yml
@@ -1,13 +1,14 @@
-
name: "API CodeQL"
on:
push:
branches: [ "master" ]
+ paths-ignore:
+ - "dashboard/**"
pull_request:
branches: [ "master" ]
paths-ignore:
- - 'app/dashboard/**'
+ - 'dashboard/**'
schedule:
- cron: '40 13 * * 2'
@@ -35,19 +36,19 @@ jobs:
language: [ 'python' ]
steps:
- - name: Checkout repository
- uses: actions/checkout@v4
-
- # Initializes the CodeQL tools for scanning.
- - name: Initialize CodeQL
- uses: github/codeql-action/init@v3
- with:
- languages: ${{ matrix.language }}
-
- - name: Autobuild
- uses: github/codeql-action/autobuild@v3
-
- - name: Perform CodeQL Analysis
- uses: github/codeql-action/analyze@v3
- with:
- category: "/language:${{matrix.language}}"
+ - name: Checkout repository
+ uses: actions/checkout@v4
+
+ # Initializes the CodeQL tools for scanning.
+ - name: Initialize CodeQL
+ uses: github/codeql-action/init@v3
+ with:
+ languages: ${{ matrix.language }}
+
+ - name: Autobuild
+ uses: github/codeql-action/autobuild@v3
+
+ - name: Perform CodeQL Analysis
+ uses: github/codeql-action/analyze@v3
+ with:
+ category: "/language:${{matrix.language}}"
diff --git a/.github/workflows/chromatic-deploy.yml b/.github/workflows/chromatic-deploy.yml
index 71f238f..2d9bd18 100644
--- a/.github/workflows/chromatic-deploy.yml
+++ b/.github/workflows/chromatic-deploy.yml
@@ -3,7 +3,7 @@ name: Chromatic Deploy
on:
push:
branches:
- - main
+ - master
pull_request:
concurrency:
@@ -34,7 +34,7 @@ jobs:
dashboard/**
chromatic:
name: Visual tests
- needs: [changed-files]
+ needs: [ changed-files ]
if: ${{ needs.changed-files.outputs.any_changed == 'true' }}
uses: ./.github/workflows/chromatic.yml
secrets: inherit
diff --git a/.github/workflows/dashboard-ci.yml b/.github/workflows/dashboard-ci.yml
index 18cbcd8..1ef2dab 100644
--- a/.github/workflows/dashboard-ci.yml
+++ b/.github/workflows/dashboard-ci.yml
@@ -3,11 +3,16 @@ name: Dashboard CI
on:
push:
branches: [ "master" ]
+ paths:
+ - "dashboard/**"
pull_request:
+ branches: [ "master" ]
+ paths:
+ - "dashboard/**"
jobs:
lint:
- env:
+ env:
VITE_BASE_API: /api/
runs-on: ubuntu-latest
defaults:
@@ -41,8 +46,8 @@ jobs:
run: pnpm lint
build:
- needs: [lint]
- env:
+ needs: [ lint ]
+ env:
VITE_BASE_API: /api/
runs-on: ubuntu-latest
defaults:
@@ -74,7 +79,7 @@ jobs:
- name: Build Dashboard
run: pnpm build --base '/dashboard/'
-
+
- name: Upload a Build Artifact
uses: actions/upload-artifact@v4.2.0
with:
@@ -82,8 +87,8 @@ jobs:
path: dashboard/dist
unit-test:
- needs: [lint, build]
- env:
+ needs: [ lint, build ]
+ env:
VITE_BASE_API: /api/
runs-on: ubuntu-latest
defaults:
diff --git a/.github/workflows/dashboard-codeql.yml b/.github/workflows/dashboard-codeql.yml
index 4671d6f..9fa7c7f 100644
--- a/.github/workflows/dashboard-codeql.yml
+++ b/.github/workflows/dashboard-codeql.yml
@@ -14,11 +14,11 @@ name: "Dashboard CodeQL"
on:
push:
branches: [ "master" ]
- paths:
+ paths:
- 'dashboard/**'
pull_request:
branches: [ "master" ]
- paths:
+ paths:
- 'dashboard/**'
schedule:
- cron: '40 13 * * 2'
@@ -47,19 +47,19 @@ jobs:
language: [ 'javascript-typescript' ]
steps:
- - name: Checkout repository
- uses: actions/checkout@v4
+ - name: Checkout repository
+ uses: actions/checkout@v4
- # Initializes the CodeQL tools for scanning.
- - name: Initialize CodeQL
- uses: github/codeql-action/init@v3
- with:
- languages: ${{ matrix.language }}
+ # Initializes the CodeQL tools for scanning.
+ - name: Initialize CodeQL
+ uses: github/codeql-action/init@v3
+ with:
+ languages: ${{ matrix.language }}
- - name: Autobuild
- uses: github/codeql-action/autobuild@v3
+ - name: Autobuild
+ uses: github/codeql-action/autobuild@v3
- - name: Perform CodeQL Analysis
- uses: github/codeql-action/analyze@v3
- with:
- category: "/language:${{matrix.language}}"
+ - name: Perform CodeQL Analysis
+ uses: github/codeql-action/analyze@v3
+ with:
+ category: "/language:${{matrix.language}}"
diff --git a/.github/workflows/package.yml b/.github/workflows/package.yml
index 36ec3d7..18678e5 100644
--- a/.github/workflows/package.yml
+++ b/.github/workflows/package.yml
@@ -33,19 +33,19 @@ on:
jobs:
build-docker-image:
runs-on: ubuntu-latest
- env:
+ env:
VITE_BASE_API: /api/
steps:
- name: Checkout
uses: actions/checkout@v4
-
+
- uses: pnpm/action-setup@v3
name: Install pnpm
with:
version: 8
run_install: false
-
- - name: Use Node.js
+
+ - name: Use Node.js
uses: actions/setup-node@v4
with:
node-version: '20.x'
@@ -53,17 +53,17 @@ jobs:
- name: Install dependencies
run: pnpm install
working-directory: ./dashboard/
-
+
- name: Build Dashboard
run: npm run build -- --outDir dist --base '/dashboard/'
working-directory: ./dashboard/
-
+
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
-
+
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
@@ -80,7 +80,7 @@ jobs:
tags: |
dawsh/marzneshin:latest
dawsh/marzneshin:${{github.ref_name}}
-
+
- name: Nightly build and push for docker
if: ${{ github.event_name == 'schedule' || github.event.inputs.build-target == 'nightly'}}
uses: docker/build-push-action@v5
@@ -90,7 +90,7 @@ jobs:
push: true
tags: |
dawsh/marzneshin:nightly
-
+
- name: Update Nightly Release
if: ${{ github.event_name == 'schedule' || github.event.inputs.build-target == 'nightly'}}
uses: andelf/nightly-release@main
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
deleted file mode 100644
index f4d3239..0000000
--- a/.github/workflows/release.yml
+++ /dev/null
@@ -1,20 +0,0 @@
-name: Release
-
-on:
- push:
- branches:
- - 'main'
-
-jobs:
- release:
- runs-on: ubuntu-latest
-
- steps:
- - name: Check out code
- uses: actions/checkout@v2
-
- - name: Release with release-please
- uses: google-github-actions/release-please-action@v2
- with:
- release-type: simple
- token: ${{ secrets.GITHUB_TOKEN }}
diff --git a/app/db/crud.py b/app/db/crud.py
index 1878726..197684a 100644
--- a/app/db/crud.py
+++ b/app/db/crud.py
@@ -22,7 +22,12 @@ from app.db.models import (
User,
)
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 (
@@ -33,7 +38,10 @@ from app.models.user import (
UserStatus,
UserUsageResponse,
)
-from app.utils.helpers import calculate_expiration_days, calculate_usage_percent
+from app.utils.helpers import (
+ calculate_expiration_days,
+ calculate_usage_percent,
+)
from config import NOTIFY_DAYS_LEFT, NOTIFY_REACHED_USAGE_PERCENT
@@ -72,8 +80,13 @@ def assure_node_inbounds(db: Session, inbounds: List[Inbound], node_id: int):
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)
+ .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:
@@ -96,7 +109,9 @@ def assure_node_inbounds(db: Session, inbounds: List[Inbound], node_id: int):
def get_node_users(
- db: Session, node_id: Optional[int], statuses: Optional[List[UserStatus]] = None
+ db: Session,
+ node_id: Optional[int],
+ statuses: Optional[List[UserStatus]] = None,
):
query = (
db.query(User.id, User.username, User.key, Inbound)
@@ -124,7 +139,11 @@ def get_user_hosts(db: Session, user_id: int):
def get_inbound_hosts(db: Session, inbound_id: int) -> List[InboundHost]:
- return db.query(InboundHost).filter(InboundHost.inbound_id == inbound_id).all()
+ return (
+ db.query(InboundHost)
+ .filter(InboundHost.inbound_id == inbound_id)
+ .all()
+ )
def get_all_inbounds(db: Session):
@@ -229,9 +248,13 @@ def get_users(
if reset_strategy:
if isinstance(reset_strategy, list):
- query = query.filter(User.data_limit_reset_strategy.in_(reset_strategy))
+ query = query.filter(
+ User.data_limit_reset_strategy.in_(reset_strategy)
+ )
else:
- query = query.filter(User.data_limit_reset_strategy == reset_strategy)
+ query = query.filter(
+ User.data_limit_reset_strategy == reset_strategy
+ )
if admin:
query = query.filter(User.admin == admin)
@@ -338,16 +361,23 @@ def update_user(db: Session, dbuser: User, modify: UserModify):
if modify.data_limit is not None:
# in case there is modification to a user's data limit
- dbuser.data_limit = modify.data_limit or None # set it to the new limit
+ dbuser.data_limit = (
+ modify.data_limit or None
+ ) # set it to the new limit
if dbuser.status is not UserStatus.expired:
# in case the user isn't already disabled/expired
- if not dbuser.data_limit or dbuser.used_traffic < dbuser.data_limit:
+ if (
+ not dbuser.data_limit
+ or dbuser.used_traffic < dbuser.data_limit
+ ):
# in case the user is unlimited or hasn't reached the limit already
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)
+ calculate_usage_percent(
+ dbuser.used_traffic, dbuser.data_limit
+ )
< NOTIFY_REACHED_USAGE_PERCENT
):
delete_notification_reminder_by_type(
@@ -375,7 +405,9 @@ def update_user(db: Session, dbuser: User, modify: UserModify):
dbuser.note = modify.note or None
if modify.data_limit_reset_strategy is not None:
- dbuser.data_limit_reset_strategy = modify.data_limit_reset_strategy.value
+ dbuser.data_limit_reset_strategy = (
+ modify.data_limit_reset_strategy.value
+ )
if modify.on_hold_timeout is not None:
dbuser.on_hold_timeout = modify.on_hold_timeout
@@ -460,7 +492,9 @@ def set_owner(db: Session, dbuser: User, admin: Admin):
def start_user_expire(db: Session, dbuser: User):
- expire = datetime.utcnow() + timedelta(seconds=dbuser.on_hold_expire_duration)
+ expire = datetime.utcnow() + timedelta(
+ seconds=dbuser.on_hold_expire_duration
+ )
dbuser.expire = expire
db.commit()
db.refresh(dbuser)
@@ -547,7 +581,9 @@ def get_admins(
def create_service(db: Session, service: ServiceCreate) -> Service:
dbservice = Service(
name=service.name,
- inbounds=db.query(Inbound).filter(Inbound.id.in_(service.inbound_ids)).all(),
+ inbounds=db.query(Inbound)
+ .filter(Inbound.id.in_(service.inbound_ids))
+ .all(),
users=[],
)
db.add(dbservice)
@@ -564,13 +600,17 @@ def get_services(db: Session) -> List[Service]:
return db.query(Service).all()
-def update_service(db: Session, db_service: Service, modification: ServiceModify):
+def update_service(
+ db: Session, db_service: Service, modification: ServiceModify
+):
if modification.name is not None:
db_service.name = modification.name
if modification.inbound_ids is not None:
db_service.inbounds = (
- db.query(Inbound).filter(Inbound.id.in_(modification.inbound_ids)).all()
+ db.query(Inbound)
+ .filter(Inbound.id.in_(modification.inbound_ids))
+ .all()
)
db.commit()
@@ -593,7 +633,9 @@ def get_node_by_id(db: Session, node_id: int):
def get_nodes(
- db: Session, status: Optional[Union[NodeStatus, list]] = None, enabled: bool = None
+ db: Session,
+ status: Optional[Union[NodeStatus, list]] = None,
+ enabled: bool = None,
):
query = db.query(Node)
@@ -698,7 +740,10 @@ def update_node_status(
def create_notification_reminder(
- db: Session, reminder_type: ReminderType, expires_at: datetime, user_id: int
+ 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
@@ -742,7 +787,9 @@ def delete_notification_reminder_by_type(
return
-def delete_notification_reminder(db: Session, dbreminder: NotificationReminder) -> None:
+def delete_notification_reminder(
+ db: Session, dbreminder: NotificationReminder
+) -> None:
db.delete(dbreminder)
db.commit()
return
diff --git a/app/db/migrations/env.py b/app/db/migrations/env.py
index b9115f1..4ee6e67 100644
--- a/app/db/migrations/env.py
+++ b/app/db/migrations/env.py
@@ -68,7 +68,9 @@ 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/migrations/versions/20faa9f18c0a_init.py b/app/db/migrations/versions/20faa9f18c0a_init.py
index a004ba3..bb29011 100644
--- a/app/db/migrations/versions/20faa9f18c0a_init.py
+++ b/app/db/migrations/versions/20faa9f18c0a_init.py
@@ -30,7 +30,9 @@ def upgrade() -> None:
sa.Column("password_reset_at", sa.DateTime(), nullable=True),
sa.PrimaryKeyConstraint("id"),
)
- op.create_index(op.f("ix_admins_username"), "admins", ["username"], unique=True)
+ op.create_index(
+ op.f("ix_admins_username"), "admins", ["username"], unique=True
+ )
jwttable = op.create_table(
"jwt",
@@ -104,14 +106,18 @@ def upgrade() -> None:
sa.PrimaryKeyConstraint("id"),
)
tls = generate_certificate()
- op.bulk_insert(tlstable, [{"id": 1, "key": tls["key"], "certificate": tls["cert"]}])
+ op.bulk_insert(
+ tlstable, [{"id": 1, "key": tls["key"], "certificate": tls["cert"]}]
+ )
op.create_table(
"inbounds",
sa.Column("id", sa.Integer(), nullable=False),
sa.Column(
"protocol",
- sa.Enum("VMess", "VLESS", "Trojan", "Shadowsocks", name="proxytypes"),
+ sa.Enum(
+ "VMess", "VLESS", "Trojan", "Shadowsocks", name="proxytypes"
+ ),
nullable=True,
),
sa.Column("tag", sa.String(length=256), nullable=False),
@@ -124,7 +130,9 @@ def upgrade() -> None:
sa.PrimaryKeyConstraint("id"),
sa.UniqueConstraint("node_id", "tag"),
)
- op.create_index(op.f("ix_inbounds_node_id"), "inbounds", ["node_id"], unique=False)
+ op.create_index(
+ op.f("ix_inbounds_node_id"), "inbounds", ["node_id"], unique=False
+ )
op.create_table(
"node_usages",
sa.Column("id", sa.Integer(), nullable=False),
@@ -146,7 +154,9 @@ def upgrade() -> None:
sa.Column("key", sa.String(length=64), nullable=True),
sa.Column(
"status",
- sa.Enum("active", "limited", "expired", "on_hold", name="userstatus"),
+ sa.Enum(
+ "active", "limited", "expired", "on_hold", name="userstatus"
+ ),
nullable=False,
),
sa.Column("used_traffic", sa.BigInteger(), nullable=True),
@@ -182,7 +192,9 @@ def upgrade() -> None:
sa.PrimaryKeyConstraint("id"),
sa.UniqueConstraint("key"),
)
- op.create_index(op.f("ix_users_username"), "users", ["username"], unique=True)
+ op.create_index(
+ op.f("ix_users_username"), "users", ["username"], unique=True
+ )
op.create_table(
"hosts",
sa.Column("id", sa.Integer(), nullable=False),
@@ -194,12 +206,16 @@ def upgrade() -> None:
sa.Column("host", sa.String(length=1024), nullable=True),
sa.Column(
"security",
- sa.Enum("inbound_default", "none", "tls", name="inboundhostsecurity"),
+ sa.Enum(
+ "inbound_default", "none", "tls", name="inboundhostsecurity"
+ ),
nullable=False,
),
sa.Column(
"alpn",
- sa.Enum("none", "h2", "http/1.1", "h2,http/1.1", name="proxyhostalpn"),
+ sa.Enum(
+ "none", "h2", "http/1.1", "h2,http/1.1", name="proxyhostalpn"
+ ),
server_default="none",
nullable=False,
),
diff --git a/app/db/migrations/versions/74e234a3bd4b_add_mux_fragment_to_hosts.py b/app/db/migrations/versions/74e234a3bd4b_add_mux_fragment_to_hosts.py
index 043cbef..0713097 100644
--- a/app/db/migrations/versions/74e234a3bd4b_add_mux_fragment_to_hosts.py
+++ b/app/db/migrations/versions/74e234a3bd4b_add_mux_fragment_to_hosts.py
@@ -21,7 +21,9 @@ def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.add_column(
"hosts",
- sa.Column("mux", sa.Boolean(), server_default=sa.sql.true(), nullable=False),
+ sa.Column(
+ "mux", sa.Boolean(), server_default=sa.sql.true(), nullable=False
+ ),
)
op.add_column("hosts", sa.Column("fragment", sa.JSON(), nullable=True))
# ### end Alembic commands ###
diff --git a/app/db/migrations/versions/e8325f600a49_user_enable.py b/app/db/migrations/versions/e8325f600a49_user_enable.py
index d1b2300..cad17db 100644
--- a/app/db/migrations/versions/e8325f600a49_user_enable.py
+++ b/app/db/migrations/versions/e8325f600a49_user_enable.py
@@ -22,7 +22,10 @@ def upgrade() -> None:
op.add_column(
"users",
sa.Column(
- "enabled", sa.Boolean(), server_default=sa.sql.true(), nullable=False
+ "enabled",
+ sa.Boolean(),
+ server_default=sa.sql.true(),
+ nullable=False,
),
)
# ### end Alembic commands ###
diff --git a/app/db/models.py b/app/db/models.py
index 255d5b8..096c95b 100644
--- a/app/db/models.py
+++ b/app/db/models.py
@@ -27,7 +27,11 @@ from app.models.proxy import (
InboundHostSecurity,
ProxyTypes,
)
-from app.models.user import ReminderType, UserDataLimitResetStrategy, UserStatus
+from app.models.user import (
+ ReminderType,
+ UserDataLimitResetStrategy,
+ UserStatus,
+)
class Admin(Base):
@@ -61,7 +65,9 @@ class Service(Base):
__tablename__ = "services"
id = Column(Integer, primary_key=True)
name = Column(String(64))
- users = relationship("User", secondary=users_services, back_populates="services")
+ users = relationship(
+ "User", secondary=users_services, back_populates="services"
+ )
inbounds = relationship(
"Inbound", secondary=inbounds_services, back_populates="services"
)
@@ -82,10 +88,16 @@ class User(Base):
username = Column(String(32), unique=True, index=True)
key = Column(String(64), unique=True)
enabled = Column(
- Boolean, nullable=False, default=True, server_default=sqlalchemy.sql.true()
+ Boolean,
+ nullable=False,
+ default=True,
+ server_default=sqlalchemy.sql.true(),
)
services = relationship(
- "Service", secondary=users_services, back_populates="users", lazy="joined"
+ "Service",
+ secondary=users_services,
+ back_populates="users",
+ lazy="joined",
)
inbounds = relationship(
"Inbound",
@@ -95,7 +107,9 @@ class User(Base):
distinct_target_key=True,
)
# proxies = relationship("Proxy", back_populates="user", cascade="all, delete-orphan")
- status = Column(Enum(UserStatus), nullable=False, default=UserStatus.active)
+ status = Column(
+ Enum(UserStatus), nullable=False, default=UserStatus.active
+ )
used_traffic = Column(BigInteger, default=0)
lifetime_used_traffic = Column(
BigInteger, default=0, server_default="0", nullable=False
@@ -150,7 +164,9 @@ class Inbound(Base):
"Service", secondary=inbounds_services, back_populates="inbounds"
)
hosts = relationship(
- "InboundHost", back_populates="inbound", cascade="all, delete, delete-orphan"
+ "InboundHost",
+ back_populates="inbound",
+ cascade="all, delete, delete-orphan",
)
@property
@@ -187,7 +203,10 @@ class InboundHost(Base):
)
mux = Column(
- Boolean, default=False, nullable=False, server_default=sqlalchemy.sql.true()
+ Boolean,
+ default=False,
+ nullable=False,
+ server_default=sqlalchemy.sql.true(),
)
fragment = Column(JSON())
@@ -231,18 +250,26 @@ class Node(Base):
address = Column(String(256))
port = Column(Integer)
xray_version = Column(String(32))
- inbounds = relationship("Inbound", back_populates="node", cascade="all, delete")
- status = Column(Enum(NodeStatus), nullable=False, default=NodeStatus.unhealthy)
+ inbounds = relationship(
+ "Inbound", back_populates="node", cascade="all, delete"
+ )
+ status = Column(
+ Enum(NodeStatus), nullable=False, default=NodeStatus.unhealthy
+ )
last_status_change = Column(DateTime, default=datetime.utcnow)
message = Column(String(1024))
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"
+ "NodeUserUsage",
+ back_populates="node",
+ cascade="all, delete, delete-orphan",
)
usages = relationship(
- "NodeUsage", back_populates="node", cascade="all, delete, delete-orphan"
+ "NodeUsage",
+ back_populates="node",
+ cascade="all, delete, delete-orphan",
)
usage_coefficient = Column(
Float, nullable=False, server_default=text("1.0"), default=1
diff --git a/app/dependencies.py b/app/dependencies.py
index 83098e7..3c95823 100644
--- a/app/dependencies.py
+++ b/app/dependencies.py
@@ -76,7 +76,8 @@ def get_user(
):
db_user = crud.get_user(db, username)
if not (
- admin.is_sudo or (db_user.admin and db_user.admin.username == admin.username)
+ admin.is_sudo
+ or (db_user.admin and db_user.admin.username == admin.username)
):
raise HTTPException(status_code=403, detail="You're not allowed")
@@ -88,7 +89,9 @@ def get_user(
def parse_start_date(start: str | None = None):
if not start:
- return datetime.fromtimestamp(datetime.utcnow().timestamp() - 30 * 24 * 3600)
+ return datetime.fromtimestamp(
+ datetime.utcnow().timestamp() - 30 * 24 * 3600
+ )
else:
return datetime.fromisoformat(start)
diff --git a/app/marzneshin.py b/app/marzneshin.py
index 0c322fc..9fc5937 100644
--- a/app/marzneshin.py
+++ b/app/marzneshin.py
@@ -68,14 +68,22 @@ app.add_middleware(
scheduler = AsyncIOScheduler(timezone="UTC")
scheduler.add_job(record_user_usages, "interval", coalesce=True, seconds=30)
-scheduler.add_job(review_users, "interval", seconds=600, coalesce=True, max_instances=1)
+scheduler.add_job(
+ review_users, "interval", seconds=600, coalesce=True, max_instances=1
+)
scheduler.add_job(reset_user_data_usage, "interval", coalesce=True, hours=1)
scheduler.add_job(
- record_realtime_bandwidth, "interval", seconds=1, coalesce=True, max_instances=1
+ record_realtime_bandwidth,
+ "interval",
+ seconds=1,
+ coalesce=True,
+ max_instances=1,
)
if config.WEBHOOK_ADDRESS:
- scheduler.add_job(send_notifications, "interval", seconds=30, replace_existing=True)
+ scheduler.add_job(
+ send_notifications, "interval", seconds=30, replace_existing=True
+ )
scheduler.add_job(
delete_expired_reminders,
"interval",
@@ -97,7 +105,9 @@ async def on_shutdown():
@app.exception_handler(RequestValidationError)
-def validation_exception_handler(request: Request, exc: RequestValidationError):
+def validation_exception_handler(
+ request: Request, exc: RequestValidationError
+):
details = {}
for error in exc.errors():
details[error["loc"][-1]] = error.get("msg")
diff --git a/app/marznode/__init__.py b/app/marznode/__init__.py
index c7a9aac..aa1f561 100644
--- a/app/marznode/__init__.py
+++ b/app/marznode/__init__.py
@@ -10,4 +10,10 @@ from .grpclib import MarzNodeGRPCLIB
nodes: Dict[int, MarzNodeBase] = {}
-__all__ = ["nodes", "operations", "MarzNodeGRPCIO", "MarzNodeGRPCLIB", "MarzNodeBase"]
+__all__ = [
+ "nodes",
+ "operations",
+ "MarzNodeGRPCIO",
+ "MarzNodeGRPCLIB",
+ "MarzNodeBase",
+]
diff --git a/app/marznode/base.py b/app/marznode/base.py
index d7f73a9..68a0509 100644
--- a/app/marznode/base.py
+++ b/app/marznode/base.py
@@ -3,7 +3,9 @@ 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/grpcio.py b/app/marznode/grpcio.py
index e3889db..7d43ffb 100644
--- a/app/marznode/grpcio.py
+++ b/app/marznode/grpcio.py
@@ -68,7 +68,9 @@ class MarzNodeGRPCIO(MarzNodeBase, MarzNodeDB):
if state != ChannelConnectivity.READY:
raise RpcError
await self._sync()
- self._streaming_task = asyncio.create_task(self._stream_user_updates())
+ self._streaming_task = asyncio.create_task(
+ self._stream_user_updates()
+ )
except RpcError:
self.synced = False
self.set_status(NodeStatus.unhealthy)
@@ -90,8 +92,12 @@ class MarzNodeGRPCIO(MarzNodeBase, MarzNodeDB):
try:
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"]],
+ user=User(
+ id=user.id, username=user.username, key=user.key
+ ),
+ inbounds=[
+ Inbound(tag=t) for t in user_update["inbounds"]
+ ],
)
)
except RpcError:
diff --git a/app/marznode/grpclib.py b/app/marznode/grpclib.py
index 1046cb5..c048bbf 100644
--- a/app/marznode/grpclib.py
+++ b/app/marznode/grpclib.py
@@ -95,8 +95,14 @@ class MarzNodeGRPCLIB(MarzNodeBase, MarzNodeDB):
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"]],
+ 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):
@@ -137,7 +143,9 @@ class MarzNodeGRPCLIB(MarzNodeBase, MarzNodeDB):
async def get_logs(self, include_buffer=True):
async with self._stub.StreamXrayLogs.open() as stm:
- await stm.send_message(XrayLogsRequest(include_buffer=include_buffer))
+ await stm.send_message(
+ XrayLogsRequest(include_buffer=include_buffer)
+ )
while True:
response = await stm.recv_message()
yield response.line
diff --git a/app/models/admin.py b/app/models/admin.py
index 9c99b0b..2f13ddd 100644
--- a/app/models/admin.py
+++ b/app/models/admin.py
@@ -5,7 +5,9 @@ from passlib.context import CryptContext
from pydantic import ConfigDict, BaseModel
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
-oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/admins/token") # Admin view url
+oauth2_scheme = OAuth2PasswordBearer(
+ tokenUrl="/api/admins/token"
+) # Admin view url
class Token(BaseModel):
@@ -37,7 +39,9 @@ class AdminModify(BaseModel):
class AdminPartialModify(AdminModify):
- __annotations__ = {k: Optional[v] for k, v in AdminModify.__annotations__.items()}
+ __annotations__ = {
+ k: Optional[v] for k, v in AdminModify.__annotations__.items()
+ }
class AdminInDB(Admin):
diff --git a/app/models/service.py b/app/models/service.py
index 5a8f119..0beb03a 100644
--- a/app/models/service.py
+++ b/app/models/service.py
@@ -23,7 +23,11 @@ class ServiceModify(Service):
inbound_ids: list[int] | None = Field(None)
model_config = ConfigDict(
json_schema_extra={
- "example": {"id": 2, "name": "my service 2", "inbound_ids": [1, 2, 3]}
+ "example": {
+ "id": 2,
+ "name": "my service 2",
+ "inbound_ids": [1, 2, 3],
+ }
}
)
diff --git a/app/models/user.py b/app/models/user.py
index 478d416..9046035 100644
--- a/app/models/user.py
+++ b/app/models/user.py
@@ -50,7 +50,9 @@ class UserDataLimitResetStrategy(str, Enum):
class User(BaseModel):
id: int | None = None
- username: Annotated[str, StringConstraints(to_lower=True, pattern=USERNAME_REGEXP)]
+ username: Annotated[
+ str, StringConstraints(to_lower=True, pattern=USERNAME_REGEXP)
+ ]
expire: Union[datetime, None, Literal[0]] = Field(None)
key: str = Field(default_factory=lambda: secrets.token_hex(16))
data_limit: int | None = Field(
@@ -97,11 +99,15 @@ class UserCreate(User):
"User cannot be on hold without a valid on_hold_expire_duration."
)
if self.expire:
- raise ValueError("User cannot be on hold with specified expire.")
+ raise ValueError(
+ "User cannot be on hold with specified expire."
+ )
elif self.status == UserStatusCreate.active and (
self.on_hold_expire_duration or self.on_hold_timeout
):
- raise ValueError("on hold parametrs set when user status isn't on_hold.")
+ raise ValueError(
+ "on hold parametrs set when user status isn't on_hold."
+ )
return self
@field_validator("expire")
@@ -112,7 +118,9 @@ class UserCreate(User):
"Expire date should be offset naive, and preferably in utc timezone."
)
if isinstance(v, datetime) and v < datetime.utcnow():
- raise ValueError("Expire date should be in the future, not in the past.")
+ raise ValueError(
+ "Expire date should be in the future, not in the past."
+ )
return v
@@ -152,7 +160,9 @@ class UserModify(User):
"Expire date should be offset naive, and preferably in utc timezone."
)
if isinstance(v, datetime) and v < datetime.utcnow():
- raise ValueError("Expire date should be in the future, not in the past.")
+ raise ValueError(
+ "Expire date should be in the future, not in the past."
+ )
return v
diff --git a/app/routes/admin.py b/app/routes/admin.py
index aa78d6b..4932dd4 100644
--- a/app/routes/admin.py
+++ b/app/routes/admin.py
@@ -16,13 +16,17 @@ from app.utils.jwt import create_admin_token
router = APIRouter(tags=["Admin"], prefix="/admins")
-def authenticate_admin(db: Session, username: str, password: str) -> Optional[Admin]:
+def authenticate_admin(
+ db: Session, username: str, password: str
+) -> Optional[Admin]:
dbadmin = crud.get_admin(db, username)
if not dbadmin:
return None
return (
- dbadmin if AdminInDB.model_validate(dbadmin).verify_password(password) else None
+ dbadmin
+ if AdminInDB.model_validate(dbadmin).verify_password(password)
+ else None
)
@@ -51,10 +55,16 @@ def get_current_admin(admin: AdminDep):
@router.post("/token", response_model=Token)
-def admin_token(form_data: Annotated[OAuth2PasswordRequestForm, Depends()], db: DBDep):
- if dbadmin := authenticate_admin(db, form_data.username, form_data.password):
+def admin_token(
+ form_data: Annotated[OAuth2PasswordRequestForm, Depends()], db: DBDep
+):
+ 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)
+ access_token=create_admin_token(
+ form_data.username, is_sudo=dbadmin.is_sudo
+ )
)
raise HTTPException(
@@ -72,8 +82,12 @@ def modify_admin(
raise HTTPException(status_code=403, detail="You're not allowed")
# If a non-sudoer admin is making itself a sudoer
- if (admin.username == username) and (modified_admin.is_sudo and not admin.is_sudo):
- raise HTTPException(status_code=403, detail="You can't make yourself sudoer!")
+ if (admin.username == username) and (
+ modified_admin.is_sudo and not admin.is_sudo
+ ):
+ raise HTTPException(
+ status_code=403, detail="You can't make yourself sudoer!"
+ )
dbadmin = crud.get_admin(db, username)
if not dbadmin:
diff --git a/app/routes/inbounds.py b/app/routes/inbounds.py
index 691c033..58018d7 100644
--- a/app/routes/inbounds.py
+++ b/app/routes/inbounds.py
@@ -78,7 +78,9 @@ def get_inbound_hosts(id: int | None, db: DBDep):
if not inbound:
raise HTTPException(status_code=404, detail="Inbound not found")
- return paginate(db, db.query(DBInboundHost).filter(DBInboundHost.inbound_id == id))
+ return paginate(
+ db, db.query(DBInboundHost).filter(DBInboundHost.inbound_id == id)
+ )
@router.post("/{id}/hosts", response_model=InboundHostResponse)
diff --git a/app/routes/node.py b/app/routes/node.py
index f2f5465..bdaa9f7 100644
--- a/app/routes/node.py
+++ b/app/routes/node.py
@@ -12,7 +12,13 @@ from starlette.websockets import WebSocketDisconnect
from app import marznode
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.dependencies import (
+ DBDep,
+ SudoAdminDep,
+ EndDateDep,
+ StartDateDep,
+ get_admin,
+)
from app.models.node import (
NodeCreate,
NodeModify,
@@ -70,7 +76,10 @@ def get_node_settings(db: DBDep, admin: SudoAdminDep):
@router.get("/usage", response_model=NodesUsageResponse)
def get_usage(
- db: DBDep, admin: SudoAdminDep, start_date: StartDateDep, end_date: EndDateDep
+ db: DBDep,
+ admin: SudoAdminDep,
+ start_date: StartDateDep,
+ end_date: EndDateDep,
):
"""
Get nodes usage
diff --git a/app/routes/service.py b/app/routes/service.py
index 7f977a8..07ab278 100644
--- a/app/routes/service.py
+++ b/app/routes/service.py
@@ -79,7 +79,11 @@ def get_service_inbounds(id: int, db: DBDep):
if not service:
raise HTTPException(status_code=404, detail="Service not found")
- query = db.query(Inbound).join(Inbound.services).where(Service.id == service.id)
+ query = (
+ db.query(Inbound)
+ .join(Inbound.services)
+ .where(Service.id == service.id)
+ )
return paginate(query)
@@ -101,10 +105,14 @@ async def modify_service(id: int, modification: ServiceModify, db: DBDep):
response = crud.update_service(db, dbservice, modification)
except sqlalchemy.exc.IntegrityError:
db.rollback()
- raise HTTPException(status_code=409, detail="problem updating the service")
+ raise HTTPException(
+ status_code=409, detail="problem updating the service"
+ )
else:
for user in response.users:
- await marznode.operations.update_user(user, old_inbounds=old_inbounds)
+ await marznode.operations.update_user(
+ user, old_inbounds=old_inbounds
+ )
return response
diff --git a/app/routes/subscription.py b/app/routes/subscription.py
index 75e5ea6..ca993da 100644
--- a/app/routes/subscription.py
+++ b/app/routes/subscription.py
@@ -43,9 +43,13 @@ def user_subscription(
user: UserResponse = UserResponse.model_validate(db_user)
if "text/html" in accept_header:
- links = generate_subscription(user=db_user, config_format="v2ray").split()
+ links = generate_subscription(
+ user=db_user, config_format="v2ray"
+ ).split()
return HTMLResponse(
- render_template(SUBSCRIPTION_PAGE_TEMPLATE, {"user": user, "links": links})
+ render_template(
+ SUBSCRIPTION_PAGE_TEMPLATE, {"user": user, "links": links}
+ )
)
response_headers = {
@@ -65,20 +69,28 @@ def user_subscription(
if re.match("^([Cc]lash-verge|[Cc]lash-?[Mm]eta)", user_agent):
conf = generate_subscription(user=db_user, config_format="clash-meta")
- return Response(content=conf, media_type="text/yaml", headers=response_headers)
+ return Response(
+ content=conf, media_type="text/yaml", headers=response_headers
+ )
elif re.match("^([Cc]lash|[Ss]tash)", user_agent):
conf = generate_subscription(user=db_user, config_format="clash")
- return Response(content=conf, media_type="text/yaml", headers=response_headers)
+ return Response(
+ content=conf, media_type="text/yaml", headers=response_headers
+ )
elif re.match("^(SFA|SFI|SFM|SFT)", user_agent):
conf = generate_subscription(user=db_user, config_format="sing-box")
return Response(
- content=conf, media_type="application/json", headers=response_headers
+ content=conf,
+ media_type="application/json",
+ headers=response_headers,
)
else:
conf = generate_subscription(
user=db_user, config_format="v2ray", as_base64=True
)
- return Response(content=conf, media_type="text/plain", headers=response_headers)
+ return Response(
+ content=conf, media_type="text/plain", headers=response_headers
+ )
@router.get("/{username}/{key}/info", response_model=UserResponse)
@@ -88,7 +100,10 @@ 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
+ db_user: SubUserDep,
+ db: DBDep,
+ start_date: StartDateDep,
+ end_date: EndDateDep,
):
usages = crud.get_user_usages(db, db_user, start_date, end_date)
@@ -122,26 +137,36 @@ def user_subscription_with_client_type(
if client_type == "clash-meta":
conf = generate_subscription(user=db_user, config_format="clash-meta")
- return Response(content=conf, media_type="text/yaml", headers=response_headers)
+ return Response(
+ content=conf, media_type="text/yaml", headers=response_headers
+ )
elif client_type == "sing-box":
conf = generate_subscription(user=db_user, config_format="sing-box")
return Response(
- content=conf, media_type="application/json", headers=response_headers
+ content=conf,
+ media_type="application/json",
+ headers=response_headers,
)
elif client_type == "clash":
conf = generate_subscription(user=db_user, config_format="clash")
- return Response(content=conf, media_type="text/yaml", headers=response_headers)
+ return Response(
+ content=conf, media_type="text/yaml", headers=response_headers
+ )
elif client_type == "v2ray":
conf = generate_subscription(
user=db_user, config_format="v2ray", as_base64=True
)
- return Response(content=conf, media_type="text/plain", headers=response_headers)
+ return Response(
+ content=conf, media_type="text/plain", headers=response_headers
+ )
elif client_type == "xray":
return Response(
content=generate_subscription(user=db_user, config_format="xray"),
headers=response_headers,
)
else:
- raise HTTPException(status_code=400, detail="Invalid subscription type")
+ raise HTTPException(
+ status_code=400, detail="Invalid subscription type"
+ )
diff --git a/app/routes/system.py b/app/routes/system.py
index 97e65ad..561cbcb 100644
--- a/app/routes/system.py
+++ b/app/routes/system.py
@@ -21,17 +21,25 @@ def get_admins_stats(db: DBDep, admin: SudoAdminDep):
def get_nodes_stats(db: DBDep, admin: SudoAdminDep):
return NodesStats(
total=db.query(Node).count(),
- healthy=db.query(Node).filter(Node.status == NodeStatus.healthy).count(),
- unhealthy=db.query(Node).filter(Node.status == NodeStatus.unhealthy).count(),
+ healthy=db.query(Node)
+ .filter(Node.status == NodeStatus.healthy)
+ .count(),
+ unhealthy=db.query(Node)
+ .filter(Node.status == NodeStatus.unhealthy)
+ .count(),
)
@router.get("/stats/users", response_model=UsersStats)
def get_users_stats(db: DBDep, admin: AdminDep):
return UsersStats(
- total=crud.get_users_count(db, admin=admin if not admin.is_sudo else None),
+ total=crud.get_users_count(
+ db, admin=admin if not admin.is_sudo else None
+ ),
active=crud.get_users_count(
- db, admin=admin if not admin.is_sudo else None, status=UserStatus.active
+ db,
+ admin=admin if not admin.is_sudo else None,
+ status=UserStatus.active,
),
on_hold=crud.get_users_count(
db,
diff --git a/app/routes/user.py b/app/routes/user.py
index b160b8c..33c5825 100644
--- a/app/routes/user.py
+++ b/app/routes/user.py
@@ -102,7 +102,9 @@ async def add_user(new_user: UserCreate, db: DBDep, admin: AdminDep):
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
@@ -147,7 +149,9 @@ def delete_expired(passed_time: int, db: DBDep, admin: AdminDep):
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)
@@ -221,7 +225,9 @@ async def remove_user(db_user: UserDep, db: DBDep, admin: AdminDep):
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 {}
@@ -236,7 +242,9 @@ def get_user_services(username: str, db: DBDep):
if not user:
raise HTTPException(status_code=404, detail="User not found")
- query = db.query(Service).join(Service.users).where(User.username == username)
+ query = (
+ db.query(Service).join(Service.users).where(User.username == username)
+ )
return paginate(query)
@@ -249,7 +257,10 @@ async def reset_user_data_usage(db_user: UserDep, db: DBDep, admin: AdminDep):
previous_status = db_user.status
db_user = crud.reset_user_data_usage(db, db_user)
- if db_user.status == UserStatus.active and previous_status == UserStatus.limited:
+ if (
+ db_user.status == UserStatus.active
+ and previous_status == UserStatus.limited
+ ):
await marznode.operations.update_user(db_user)
user = UserResponse.model_validate(db_user)
@@ -299,7 +310,9 @@ async def disable_user(db_user: UserDep, db: DBDep, admin: AdminDep):
@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)
"""
@@ -329,7 +342,9 @@ def get_user_usage(
@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")
@@ -341,6 +356,8 @@ def set_owner(username: str, admin_username: str, db: DBDep, admin: SudoAdminDep
db_user = crud.set_owner(db, db_user, new_admin)
user = UserResponse.model_validate(db_user)
- logger.info("`%s`'s owner successfully set to `%s`", user.username, admin.username)
+ logger.info(
+ "`%s`'s owner successfully set to `%s`", user.username, admin.username
+ )
return user
diff --git a/app/tasks/record_usages.py b/app/tasks/record_usages.py
index 30ddc56..cdecf01 100644
--- a/app/tasks/record_usages.py
+++ b/app/tasks/record_usages.py
@@ -11,17 +11,22 @@ from app.db.models import NodeUsage, NodeUserUsage, User
from app.marznode import MarzNodeBase
-def record_user_usage_logs(params: list, node_id: int, consumption_factor: int = 1):
+def record_user_usage_logs(
+ 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
+ NodeUserUsage.node_id == node_id,
+ NodeUserUsage.created_at == created_at,
)
)
existings = [r[0] for r in db.execute(select_stmt).fetchall()]
@@ -67,13 +72,18 @@ def record_node_stats(node_id: int, usage: int):
if not usage:
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)
+ and_(
+ NodeUsage.node_id == node_id,
+ NodeUsage.created_at == created_at,
+ )
)
notfound = db.execute(select_stmt).first() is None
if notfound:
@@ -89,7 +99,10 @@ def record_node_stats(node_id: int, usage: int):
downlink=NodeUsage.downlink + usage,
)
.where(
- and_(NodeUsage.node_id == node_id, NodeUsage.created_at == created_at)
+ and_(
+ NodeUsage.node_id == node_id,
+ NodeUsage.created_at == created_at,
+ )
)
)
@@ -97,7 +110,9 @@ def record_node_stats(node_id: int, usage: int):
db.commit()
-async def get_users_stats(node_id: int, node: MarzNodeBase) -> tuple[int, list[dict]]:
+async def get_users_stats(
+ node_id: int, node: MarzNodeBase
+) -> tuple[int, list[dict]]:
try:
params = list()
for stat in await node.fetch_users_stats():
@@ -112,14 +127,19 @@ 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
+ node.usage_coefficient
+ if (node := marznode.nodes.get(node_id))
+ else 1
)
node_usage = 0
for param in params:
@@ -143,12 +163,18 @@ async def record_user_usages():
online_at=datetime.utcnow(),
)
- db.execute(stmt, users_usage, execution_options={"synchronize_session": None})
+ db.execute(
+ stmt, users_usage, execution_options={"synchronize_session": None}
+ )
db.commit()
for node_id, params in api_params.items():
record_user_usage_logs(
params,
node_id,
- node.usage_coefficient if (node := marznode.nodes.get(node_id)) else 1,
+ (
+ node.usage_coefficient
+ if (node := marznode.nodes.get(node_id))
+ else 1
+ ),
)
diff --git a/app/tasks/reset_user_data_usage.py b/app/tasks/reset_user_data_usage.py
index 3903b0b..3e009e5 100644
--- a/app/tasks/reset_user_data_usage.py
+++ b/app/tasks/reset_user_data_usage.py
@@ -28,7 +28,9 @@ async def reset_user_data_usage():
],
):
last_reset_time = user.traffic_reset_at or user.created_at
- num_days_to_reset = reset_strategy_to_days[user.data_limit_reset_strategy]
+ num_days_to_reset = reset_strategy_to_days[
+ user.data_limit_reset_strategy
+ ]
if not (now - last_reset_time).days >= num_days_to_reset:
continue
diff --git a/app/tasks/review_users.py b/app/tasks/review_users.py
index fb7a176..50549f8 100644
--- a/app/tasks/review_users.py
+++ b/app/tasks/review_users.py
@@ -15,8 +15,15 @@ from app.db import (
)
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
@@ -29,7 +36,9 @@ 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)
+ 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)
):
@@ -44,10 +53,16 @@ def add_notification_reminders(
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)
+ 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,
)
@@ -72,7 +87,9 @@ async def review_users():
asyncio.create_task(
report.status_change(
- user.username, user.status, UserResponse.model_validate(user)
+ user.username,
+ user.status,
+ UserResponse.model_validate(user),
)
)
@@ -100,7 +117,9 @@ async def review_users():
start_user_expire(db, user)
asyncio.create_task(
report.status_change(
- user.username, user.status, UserResponse.model_validate(user)
+ user.username,
+ user.status,
+ UserResponse.model_validate(user),
)
)
logger.info(f"on hold user `{user.username}` has been activated")
diff --git a/app/tasks/send_notifications.py b/app/tasks/send_notifications.py
index 36e04df..f9aaac7 100644
--- a/app/tasks/send_notifications.py
+++ b/app/tasks/send_notifications.py
@@ -16,7 +16,11 @@ logger = logging.getLogger(__name__)
session = Session()
-headers = {"x-webhook-secret": config.WEBHOOK_SECRET} if config.WEBHOOK_SECRET else None
+headers = (
+ {"x-webhook-secret": config.WEBHOOK_SECRET}
+ if config.WEBHOOK_SECRET
+ else None
+)
async def send(data: List[Dict[Any, Any]]) -> bool:
@@ -29,7 +33,9 @@ async def send(data: List[Dict[Any, Any]]) -> bool:
bool: returns True if an ok response received
"""
try:
- logger.debug(f"Sending {len(data)} webhook updates to {config.WEBHOOK_ADDRESS}")
+ logger.debug(
+ f"Sending {len(data)} webhook updates to {config.WEBHOOK_ADDRESS}"
+ )
r = await aiohttp.request(
"post", config.WEBHOOK_ADDRESS, json=data, headers=headers
@@ -62,14 +68,21 @@ async def send_notifications():
if not notifications_to_send:
return
- if not await send([jsonable_encoder(notif) for notif in notifications_to_send]):
+ if not await send(
+ [jsonable_encoder(notif) for notif in notifications_to_send]
+ ):
for notification in notifications_to_send:
- if (notification.tries + 1) > config.NUMBER_OF_RECURRENT_NOTIFICATIONS:
+ if (
+ notification.tries + 1
+ ) > config.NUMBER_OF_RECURRENT_NOTIFICATIONS:
continue
notification.tries += 1
- notification.send_at = ( # schedule notification for n seconds later
- dt.utcnow() + td(seconds=config.RECURRENT_NOTIFICATIONS_TIMEOUT)
- ).timestamp()
+ notification.send_at = (
+ ( # schedule notification for n seconds later
+ dt.utcnow()
+ + td(seconds=config.RECURRENT_NOTIFICATIONS_TIMEOUT)
+ ).timestamp()
+ )
queue.append(notification)
diff --git a/app/telegram/report.py b/app/telegram/report.py
index 8a73630..70eaa61 100644
--- a/app/telegram/report.py
+++ b/app/telegram/report.py
@@ -21,7 +21,10 @@ async def report(message: str, parse_mode="html", keyboard=None):
else:
for admin in TELEGRAM_ADMIN_ID:
await telegram.bot.send_message(
- admin, message, parse_mode=parse_mode, reply_markup=keyboard
+ admin,
+ message,
+ parse_mode=parse_mode,
+ reply_markup=keyboard,
)
except ApiTelegramException as e:
logger.error(e)
@@ -48,7 +51,9 @@ async def report_new_user(
username=escape_html(username),
data_limit=readable_size(data_limit) if data_limit else "Unlimited",
expire_date=(
- expire_date.strftime("%H:%M:%S %Y-%m-%d") if expire_date else "Never"
+ 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]),
)
@@ -57,7 +62,11 @@ async def report_new_user(
async def report_user_modification(
- username: str, expire_date: datetime, data_limit: int, services: list, by: str
+ username: str,
+ expire_date: datetime,
+ data_limit: int,
+ services: list,
+ by: str,
):
text = """\
✏️ <b>#Modified</b>
@@ -73,7 +82,9 @@ async def report_user_modification(
username=escape_html(username),
data_limit=readable_size(data_limit) if data_limit else "Unlimited",
expire_date=(
- expire_date.strftime("%H:%M:%S %Y-%m-%d") if expire_date else "Never"
+ expire_date.strftime("%H:%M:%S %Y-%m-%d")
+ if expire_date
+ else "Never"
),
services=", ".join([s.name for s in services]),
)
diff --git a/app/utils/crypto.py b/app/utils/crypto.py
index 779e8e7..6f61f7c 100644
--- a/app/utils/crypto.py
+++ b/app/utils/crypto.py
@@ -26,7 +26,9 @@ def generate_certificate():
cert.set_issuer(cert.get_subject())
cert.set_pubkey(k)
cert.sign(k, "sha512")
- cert_pem = crypto.dump_certificate(crypto.FILETYPE_PEM, cert).decode("utf-8")
+ 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}
diff --git a/app/utils/jwt.py b/app/utils/jwt.py
index 09e882d..508625a 100644
--- a/app/utils/jwt.py
+++ b/app/utils/jwt.py
@@ -21,7 +21,9 @@ def create_admin_token(username: str, is_sudo=False) -> str:
"iat": datetime.utcnow(),
}
if JWT_ACCESS_TOKEN_EXPIRE_MINUTES > 0:
- expire = datetime.utcnow() + timedelta(minutes=JWT_ACCESS_TOKEN_EXPIRE_MINUTES)
+ expire = datetime.utcnow() + timedelta(
+ minutes=JWT_ACCESS_TOKEN_EXPIRE_MINUTES
+ )
data["exp"] = expire
encoded_jwt = jwt.encode(data, get_secret_key(), algorithm="HS256")
return encoded_jwt
diff --git a/app/utils/report.py b/app/utils/report.py
index b588c95..4cdbea4 100644
--- a/app/utils/report.py
+++ b/app/utils/report.py
@@ -23,7 +23,10 @@ from app.utils.notification import (
async def status_change(
- username: str, status: UserStatus, user: UserResponse, by: Optional[Admin] = None
+ username: str,
+ status: UserStatus,
+ user: UserResponse,
+ by: Optional[Admin] = None,
) -> None:
try:
await telegram.report_status_change(username, status)
@@ -32,13 +35,17 @@ async def status_change(
if status == UserStatus.limited:
await notify(
UserLimited(
- username=username, action=Notification.Type.user_limited, user=user
+ 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
+ username=username,
+ action=Notification.Type.user_expired,
+ user=user,
)
)
elif status == UserStatus.active:
@@ -100,7 +107,9 @@ async def user_deleted(username: str, by: Admin) -> None:
except Exception:
pass
await notify(
- UserDeleted(username=username, action=Notification.Type.user_deleted, by=by)
+ UserDeleted(
+ username=username, action=Notification.Type.user_deleted, by=by
+ )
)
@@ -148,7 +157,9 @@ async def data_usage_percent_reached(
expire: Optional[int] = None,
) -> None:
await notify(
- ReachedUsagePercent(username=user.username, user=user, used_percent=percent)
+ ReachedUsagePercent(
+ username=user.username, user=user, used_percent=percent
+ )
)
await create_notification_reminder(
db,
@@ -161,7 +172,9 @@ async def data_usage_percent_reached(
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 notify(
+ ReachedDaysLeft(username=user.username, user=user, days_left=days)
+ )
await create_notification_reminder(
db,
ReminderType.expiration_date,
diff --git a/app/utils/share.py b/app/utils/share.py
index f1e2e61..2e6d6b0 100644
--- a/app/utils/share.py
+++ b/app/utils/share.py
@@ -9,7 +9,13 @@ from typing import Literal, Union, List
from uuid import UUID
from jdatetime import date as jd
-from v2share import V2Data, SingBoxConfig, ClashConfig, ClashMetaConfig, XrayConfig
+from v2share import (
+ V2Data,
+ SingBoxConfig,
+ ClashConfig,
+ ClashMetaConfig,
+ XrayConfig,
+)
from v2share.links import LinksConfig
from app.db import GetDB, crud
@@ -73,7 +79,9 @@ def generate_subscription(
subscription_handler_class = subscription_handlers[config_format]
if template_path := handlers_templates[subscription_handler_class]:
- subscription_handler = subscription_handler_class(template_path=template_path)
+ subscription_handler = subscription_handler_class(
+ template_path=template_path
+ )
else:
subscription_handler = subscription_handler_class()
@@ -81,7 +89,9 @@ def generate_subscription(
inbounds, key, format_variables, conf=subscription_handler
)
- return config if not as_base64 else base64.b64encode(config.encode()).decode()
+ return (
+ config if not as_base64 else base64.b64encode(config.encode()).decode()
+ )
def format_time_left(seconds_left: int) -> str:
@@ -118,7 +128,9 @@ def setup_format_variables(extra_data: dict) -> dict:
seconds_left = (expire_datetime - dt.utcnow()).total_seconds()
expire_date = expire_datetime.date()
jalali_expire_date = jd.fromgregorian(
- year=expire_date.year, month=expire_date.month, day=expire_date.day
+ year=expire_date.year,
+ month=expire_date.month,
+ day=expire_date.day,
).strftime("%Y-%m-%d")
days_left = (expire_datetime - dt.utcnow()).days + 1
time_left = format_time_left(seconds_left)
@@ -128,7 +140,10 @@ def setup_format_variables(extra_data: dict) -> dict:
expire_date = "∞"
jalali_expire_date = "∞"
else:
- if on_hold_expire_duration is not None and on_hold_expire_duration >= 0:
+ if (
+ on_hold_expire_duration is not None
+ and on_hold_expire_duration >= 0
+ ):
days_left = timedelta(seconds=on_hold_expire_duration).days
time_left = format_time_left(on_hold_expire_duration)
expire_date = "-"
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 0000000..2e751e8
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,3 @@
+[tool.black]
+line-length = 79
+extend-exclude = ".*_(grpc|pb2).pyi?"
\ No newline at end of file