From 6aabe74f3ba4ce6c5d84e79e925fa2c966deea6d Mon Sep 17 00:00:00 2001 From: Erfan <143827987+erfjab@users.noreply.github.com> Date: Fri, 5 Apr 2024 13:49:29 +0330 Subject: [PATCH 1/5] Update README.md --- README.md | 363 ------------------------------------------------------ 1 file changed, 363 deletions(-) diff --git a/README.md b/README.md index 3a7c006..56c2347 100644 --- a/README.md +++ b/README.md @@ -1,365 +1,2 @@ -# marzpy -A Python library that helps you easily use [Marzban](https://github.com/Gozargah/Marzban)'s API panel >[!IMPORTANT] >**Status:** Working on new update 🔥 -## installation -```shell -pip install marzpy -``` -requirements : ```requests``` -# How To Use -```python -from marzpy import Marzban - -panel = Marzban("username","password","https://example.com") - -mytoken = panel.get_token() - -# panel.anyfunction() - -``` -# Features - -- Admin - - [get token](#get-token) - - [get admin](#get-current-admin) - - [create admin](#create-admin) - - [modify admin](#modify-admin) - - [remove admin](#remove-admin) - - [get all admins](#get-all-admins) -- Subscription - - [user subscription](#user-subscription) - - [user subscription info](#user-subscription-info) -- System - - [get system stats](#get-system-stats) - - [get inbounds](#get-inbounds) - - [get hosts](#get-hosts) - - [modify hosts](#modify-hosts) -- Core - - [get core stats](#get-core-stats) - - [restart core](#restart-core) - - [get core config](#get-core-config) - - [modify core config](#modify-core-config) -- User - - [add user](#add-user) - - [get user](#get-user) - - [modify user](#modify-user) - - [remove user](#remove-user) - - [reset user data usage](#reset-user-data-usage) - - [reset all users data usage](#reset-all-users-data-usage) - - [get all users](#get-all-users) - - [get user usage](#get-user-usage) -- User Template - - [get all user templates](#get-all-user-templates) - - [add user template](#add-user-template) - - [get user template](#get-user-template) - - [modify user template](#modify-user-template) - - [remove user template](#remove-user-template) -- Node - - [add node](#add-node) - - [get node](#get-node) - - [modify node](#modify-node) - - [remove node](#remove-node) - - [get all nodes](#get-all-nodes) - - [reconenct node](#reconenct-node) - - [get all nodes usage](#get-node-usage) - - -## Thanks To - -- [ErfanTech](https://github.com/ErfanTech) :laughing: - -# Examples -### Get Token -```python - -from marzpy import Marzban - -panel = Marzban("username","password","https://example.com") - -mytoken = panel.get_token() - -``` -### Get Current admin -```python -admin = panel.get_current_admin(token=mytoken) -print(admin) #output: {'username': 'admin', 'is_sudo': True} -``` -### Create Admin -```python -info = {'username':'test','password':'pasword','is_sudo':False} -rsault = panel.create_admin(token=mytoken,data=info) -print(result) #output: success -``` -### Modify Admin -```python -target_admin = "test" -info = {'password':'newpassword','is_sudo':False} -result = panel.change_admin_password(username=target_admin,token=mytoken,data=info) -print(result) #output: success -``` -### Remove Admin -```python -target_admin = "test" -result = panel.delete_admin(username=target_admin,token=mytoken) -print(result) #output: success -``` -### Get All Admins -```python -result = panel.get_all_admins(token=mytoken) -print(result) -#output: [{'username': 'test', 'is_sudo': True}, {'username': 'test1', 'is_sudo': False}] -``` -### User Subscription -```python -subscription_url = "https://sub.yourdomain.com/sub/eyJhbGciOiJIUzI8NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJNbWRDcmFaeSIsImFjY2VzcyI8InN1YnNjcmlwdGlvbiIsImlhdCI1MTY5NDk1NTkxMH0.o75ML5835SPXpVPKXcvEIUxMTwSy-4XGS9NIdWOAmXY" -result = panel.get_subscription(subscription_url) -print(result) #output: Configs -``` -### User Subscription info -```python -subscription_url = "https://sub.yourdomain.com/sub/eyJhbGciOiJIUzI8NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJNbWRDcmFaeSIsImFjY2VzcyI8InN1YnNjcmlwdGlvbiIsImlhdCI1MTY5NDk1NTkxMH0.o75ML5835SPXpVPKXcvEIUxMTwSy-4XGS9NIdWOAmXY" -result = panel.get_subscription_info(subscription_url) -print(result) #output: User information (usage,links,inbounds,....) -``` -### Get System Stats -```python -result = panel.get_system_stats(token=mytoken) -print(result) #output: system stats Memory & CPU usage ... -``` -### Get Inbounds -```python -result = panel.get_inbounds(token=mytoken) -print(result) #output: list of inbounds -``` -### Get Hosts -```python -result = panel.get_hosts(token=mytoken) -print(result) #output: list of hosts -``` -### Modify Hosts -```python -hosts = { - "VMess TCP": [ - { - "remark": "somename", - "address": "someaddress", - "port": 0, - "sni": "somesni", - "host": "somehost", - "security": "inbound_default", - "alpn": "", - "fingerprint": "" - } - ] -} -# **Backup first** -result = panel.modify_hosts(token=mytoken,data=hosts) -print(result) #output: hosts -``` -### Get Core Stats -```python -result = panel.get_xray_core(token=mytoken) -print(result) - #output: {'version': '1.8.1', 'started': True, 'logs_websocket': '/api/core/logs'} -``` -### Restart Core -```python -result = panel.restart_xray_core(token=mytoken) -print(result) - #output: success -``` -### Get Core Config -```python -result = panel.get_xray_config(token=mytoken) -print(result) #output: your xray core config -``` -### Modify Core Config -```python -new_config={"your config"} -result = panel.modify_xray_config(token=mytoken,config=new_config) -print(result) #output: success -``` -### Add User -```python -from marzpy.api.user import User - -user = User( - username="Mewhrzad", - proxies={ - "vmess": {"id": "35e7e39c-7d5c-1f4b-8b71-508e4f37ff53"}, - "vless": {"id": "35e7e39c-7d5c-1f4b-8b71-508e4f37ff53"}, - }, - inbounds={"vmess": ["VMess TCP"], "vless": ["VLESS TCP REALITY"]}, - expire=0, - data_limit=0, - data_limit_reset_strategy="no_reset", -) -result = panel.add_user(user=user, token=token) #return new User object - -print(result.username) #-> Mewhrzad, #user.proxies, #user.inbounds, #user.expire, #user.data_limit, #userdata_limit_reset_strategy, #user.status, #user.used_traffic, #user.lifetime_used_traffic, #user.created_at, #user.links, #user.subscription_url, #user.excluded_inbounds -``` -### Get User -```python -result = panel.get_user("Mewhrzad",token=mytoken) #return User object -print(result.subscription_url) -``` -### Modify User -```python -new_user = User( - username="test", - proxies={ - "vmess": {"id": "35e4e39c-7d5c-4f4b-8b71-558e4f37ff53"}, - "vless": {"id": "35e4e39c-7d5c-4f4b-8b71-558e4f37ff53"}, - }, - inbounds={"vmess": ["VMess TCP"], "vless": ["VLESS TCP REALITY"]}, - expire=0, - data_limit=0, - data_limit_reset_strategy="no_reset", - status="active", -) -result = panel.modify_user("Mewhrzad", token=mytoken, user=new_user) -print(result.subscription_url) #output: modified user object -``` -### Remove User -```python -result = panel.delete_user("test", token=mytoken) -print(result) #output: success -``` -### Reset User Data Usage -```python -result = panel.reset_user_traffic("test", token=mytoken) -print(result) #output: success -``` -### Reset All Users Data Usage -```python -result = panel.reset_all_users_traffic(token=mytoken) -print(result) #output: success -``` -### Get All Users -```python -result = panel.get_all_users(token=mytoken) #return list of users -for user in result: - print(user.username) -``` -### Get User Usage -```python -result = panel.get_user_usage("mewhrzad",token=mytoken) -print(result) -#output: [{'node_id': None, 'node_name': 'MTN', 'used_traffic': 0}, -#{'node_id': 1, 'node_name': 'MCI', 'used_traffic': 0}] -``` -### Get All User Templates -```python -result = panel.get_all_templates(token=mytoken) #return template list object -for template in result: - print(template.name) -``` -### Add User Template -```python -from marzpy.api.template import Template - -temp = Template( - name="new_template", - inbounds={"vmess": ["VMESS TCP"], "vless": ["VLESS TCP REALITY"]}, - data_limit=0, - expire_duration=0, - username_prefix=None, - username_suffix=None, -) -result = panel.add_template(token=mytoken, template=temp) # return new Template object -print(result.name) #output: new_template -``` -### Get User Template -```python -template_id = 11 -result = panel.get_template_by_id(token=mytoken, id=template_id) # return Template object -print(result.name) #output: new_template -``` -### Modify User Template -```python -from marzpy.api.template import Template - -temp = Template( - name="new_template2", - inbounds={"vmess": ["VMESS TCP"], "vless": ["VLESS TCP REALITY"]}, - data_limit=0, - expire_duration=0, - username_prefix=None, - username_suffix=None, -) -result = panel.modify_template_by_id( - id=1, token=mytoken, template=temp) # return Modified Template object -print(result.name) #output: new_template2 -``` -### Remove User Template -```python -result = panel.delete_template_by_id(id=1, token=mytoken) -print(result) #output: success -``` -### Add Node -```python -from marzpy.api.node import Node - -my_node = Node( - name="somename", - address="test.example.com", - port=62050, - api_port=62051, - certificate="your_cert", - id=4, - xray_version="1.8.1", - status="connected", - message="string", -) - -result = panel.add_node(token=mytoken, node=my_node) # return new Node object -print(result.address) -``` -### Get Node -```python -result = panel.get_node_by_id(id=1, token=mytoken) # return exist Node object -print(result.address) #output: address of node 1 -``` -### Modify Node -```python -from marzpy.api.node import Node - -my_node = Node( - name="somename", - address="test.example.com", - port=62050, - api_port=62051, - certificate="your_cert", - id=4, - xray_version="1.8.1", - status="connected", - message="string", -) - -result = panel.modify_node_by_id(id=1, token=mytoken,node=my_node) # return modified Node object -print(result.address) #output:test.example.com -``` -### Remove Node -```python -result = panel.delete_node(id=1, token=mytoken) -print(result) #output: success -``` -### Get All Nodes -```python -result = panel.get_all_nodes(token=mytoken) # return List of Node object -for node in result: - print(node.address) -``` -### Reconenct Node -```python -result = panel.reconnect_node(id=1,token=mytoken) -print(result) #output: success -``` -### Get Node Usage -```python -result = panel.get_nodes_usage(token=mytoken) -for node in result: - print(node) -#output:{'node_id': 1, 'node_name': 'N1', 'uplink': 1000000000000, 'downlink': 1000000000000} -# {'node_id': 2, 'node_name': 'N2', 'uplink': 1000000000000, 'downlink': 1000000000000} -``` From af9e7ddc2e48c46686b63d3adec7e03be1b1c362 Mon Sep 17 00:00:00 2001 From: mewhrzad Date: Sat, 13 Apr 2024 00:33:56 +0330 Subject: [PATCH 2/5] Async Friendly --- .github/workflows/python-package.yml | 40 +++ .gitignore | 161 ++++++++++++ LICENSE | 21 ++ README.md | 364 +++++++++++++++++++++++++++ marzpy/__init__.py | 1 + marzpy/api/__init__.py | 14 ++ marzpy/api/admin.py | 99 ++++++++ marzpy/api/core.py | 53 ++++ marzpy/api/node.py | 137 ++++++++++ marzpy/api/send_requests.py | 28 +++ marzpy/api/subscription.py | 34 +++ marzpy/api/system.py | 50 ++++ marzpy/api/template.py | 99 ++++++++ marzpy/api/user.py | 201 +++++++++++++++ marzpy/marzban.py | 9 + setup.py | 17 ++ 16 files changed, 1328 insertions(+) create mode 100644 .github/workflows/python-package.yml create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 README.md create mode 100644 marzpy/__init__.py create mode 100644 marzpy/api/__init__.py create mode 100644 marzpy/api/admin.py create mode 100644 marzpy/api/core.py create mode 100644 marzpy/api/node.py create mode 100644 marzpy/api/send_requests.py create mode 100644 marzpy/api/subscription.py create mode 100644 marzpy/api/system.py create mode 100644 marzpy/api/template.py create mode 100644 marzpy/api/user.py create mode 100644 marzpy/marzban.py create mode 100644 setup.py diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml new file mode 100644 index 0000000..fd04aae --- /dev/null +++ b/.github/workflows/python-package.yml @@ -0,0 +1,40 @@ +# This workflow will install Python dependencies, run tests and lint with a variety of Python versions +# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python + +name: Python package + +on: + push: + branches: [ "main" ] + pull_request: + branches: [ "main" ] + +jobs: + build: + + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + python-version: ["3.9", "3.10", "3.11"] + + steps: + - uses: actions/checkout@v3 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v3 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: | + python -m pip install --upgrade pip + python -m pip install flake8 pytest + if [ -f requirements.txt ]; then pip install -r requirements.txt; fi + - name: Lint with flake8 + run: | + # stop the build if there are Python syntax errors or undefined names + flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics + # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide + flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics + - name: Test with pytest + run: | + pytest diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..462f787 --- /dev/null +++ b/.gitignore @@ -0,0 +1,161 @@ +eg.py +.vscode +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/#use-with-ide +.pdm.toml + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..f6d3698 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 Mehrzad Khosravi + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..ea69efd --- /dev/null +++ b/README.md @@ -0,0 +1,364 @@ +# marzpy +A Python library that helps you easily use [Marzban](https://github.com/Gozargah/Marzban)'s API panel +## installation +```shell +pip install marzpy +``` +requirements : ```requests``` +# How To Use +```python +from marzpy import Marzban +import asyncio + +async def main(): + panel = Marzban("username","password","https://example.com") + token = await panel.get_token() + #await panel.anyfunction(token) + +asyncio.run(main()) +``` +# Features + +- Admin + - [get token](#get-token) + - [get admin](#get-current-admin) + - [create admin](#create-admin) + - [modify admin](#modify-admin) + - [remove admin](#remove-admin) + - [get all admins](#get-all-admins) +- Subscription + - [user subscription](#user-subscription) + - [user subscription info](#user-subscription-info) +- System + - [get system stats](#get-system-stats) + - [get inbounds](#get-inbounds) + - [get hosts](#get-hosts) + - [modify hosts](#modify-hosts) +- Core + - [get core stats](#get-core-stats) + - [restart core](#restart-core) + - [get core config](#get-core-config) + - [modify core config](#modify-core-config) +- User + - [add user](#add-user) + - [get user](#get-user) + - [modify user](#modify-user) + - [remove user](#remove-user) + - [reset user data usage](#reset-user-data-usage) + - [reset all users data usage](#reset-all-users-data-usage) + - [get all users](#get-all-users) + - [get user usage](#get-user-usage) +- User Template + - [get all user templates](#get-all-user-templates) + - [add user template](#add-user-template) + - [get user template](#get-user-template) + - [modify user template](#modify-user-template) + - [remove user template](#remove-user-template) +- Node + - [add node](#add-node) + - [get node](#get-node) + - [modify node](#modify-node) + - [remove node](#remove-node) + - [get all nodes](#get-all-nodes) + - [reconenct node](#reconenct-node) + - [get all nodes usage](#get-node-usage) + - +## Thanks To + +- [ErfanTech](https://github.com/ErfanTech) :laughing: + +# Examples +### Get Token +```python + +from marzpy import Marzban + +panel = Marzban("username","password","https://example.com") + +mytoken = await panel.get_token() + +``` +### Get Current admin +```python +admin = await panel.get_current_admin(token=mytoken) +print(admin) #output: {'username': 'mewhrzad', 'is_sudo': True, 'telegram_id': 123456789, 'discord_webhook': None} +``` +### Create Admin +```python +info = {'username':'test','password':'pasword','is_sudo':False} +result = await panel.create_admin(token=mytoken,data=info) +print(result) #output: success +``` +### Modify Admin +```python +target_admin = "test" +info = {'password':'newpassword','is_sudo':False} +result = await panel.modify_admin(username=target_admin,token=mytoken,data=info) +print(result) #output: success +``` +### Remove Admin +```python +target_admin = "test" +result = panel.remove_admin(username=target_admin,token=mytoken) +print(result) #output: success +``` +### Get All Admins +```python +result = panel.get_all_admins(token=mytoken) +print(result) +#output: [{'username': 'test', 'is_sudo': True}, {'username': 'test1', 'is_sudo': False}] +``` +### User Subscription +```python +subscription_url = "https://sub.yourdomain.com/sub/eyJhbGciOiJIUzI8NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJNbWRDcmFaeSIsImFjY2VzcyI8InN1YnNjcmlwdGlvbiIsImlhdCI1MTY5NDk1NTkxMH0.o75ML5835SPXpVPKXcvEIUxMTwSy-4XGS9NIdWOAmXY" +result = panel.get_subscription(subscription_url) +print(result) #output: Configs +``` +### User Subscription info +```python +subscription_url = "https://sub.yourdomain.com/sub/eyJhbGciOiJIUzI8NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJNbWRDcmFaeSIsImFjY2VzcyI8InN1YnNjcmlwdGlvbiIsImlhdCI1MTY5NDk1NTkxMH0.o75ML5835SPXpVPKXcvEIUxMTwSy-4XGS9NIdWOAmXY" +result = panel.get_subscription_info(subscription_url) +print(result) #output: User information (usage,links,inbounds,....) +``` +### Get System Stats +```python +result = panel.get_system_stats(token=mytoken) +print(result) #output: system stats Memory & CPU usage ... +``` +### Get Inbounds +```python +result = panel.get_inbounds(token=mytoken) +print(result) #output: list of inbounds +``` +### Get Hosts +```python +result = panel.get_hosts(token=mytoken) +print(result) #output: list of hosts +``` +### Modify Hosts +```python +hosts = { + "VMess TCP": [ + { + "remark": "somename", + "address": "someaddress", + "port": 0, + "sni": "somesni", + "host": "somehost", + "security": "inbound_default", + "alpn": "", + "fingerprint": "" + } + ] +} +# **Backup first** +result = panel.modify_hosts(token=mytoken,data=hosts) +print(result) #output: hosts +``` +### Get Core Stats +```python +result = panel.get_xray_core(token=mytoken) +print(result) + #output: {'version': '1.8.1', 'started': True, 'logs_websocket': '/api/core/logs'} +``` +### Restart Core +```python +result = panel.restart_xray_core(token=mytoken) +print(result) + #output: success +``` +### Get Core Config +```python +result = panel.get_xray_config(token=mytoken) +print(result) #output: your xray core config +``` +### Modify Core Config +```python +new_config={"your config"} +result = panel.modify_xray_config(token=mytoken,config=new_config) +print(result) #output: success +``` +### Add User +```python +from marzpy.api.user import User + +user = User( + username="Mewhrzad", + proxies={ + "vmess": {"id": "35e7e39c-7d5c-1f4b-8b71-508e4f37ff53"}, + "vless": {"id": "35e7e39c-7d5c-1f4b-8b71-508e4f37ff53"}, + }, + inbounds={"vmess": ["VMess TCP"], "vless": ["VLESS TCP REALITY"]}, + expire=0, + data_limit=0, + data_limit_reset_strategy="no_reset", +) +result = panel.add_user(user=user, token=token) #return new User object + +print(result.username) #-> Mewhrzad, #user.proxies, #user.inbounds, #user.expire, #user.data_limit, #userdata_limit_reset_strategy, #user.status, #user.used_traffic, #user.lifetime_used_traffic, #user.created_at, #user.links, #user.subscription_url, #user.excluded_inbounds +``` +### Get User +```python +result = panel.get_user("Mewhrzad",token=mytoken) #return User object +print(result.subscription_url) +``` +### Modify User +```python +new_user = User( + username="test", + proxies={ + "vmess": {"id": "35e4e39c-7d5c-4f4b-8b71-558e4f37ff53"}, + "vless": {"id": "35e4e39c-7d5c-4f4b-8b71-558e4f37ff53"}, + }, + inbounds={"vmess": ["VMess TCP"], "vless": ["VLESS TCP REALITY"]}, + expire=0, + data_limit=0, + data_limit_reset_strategy="no_reset", + status="active", +) +result = panel.modify_user("Mewhrzad", token=mytoken, user=new_user) +print(result.subscription_url) #output: modified user object +``` +### Remove User +```python +result = panel.delete_user("test", token=mytoken) +print(result) #output: success +``` +### Reset User Data Usage +```python +result = panel.reset_user_traffic("test", token=mytoken) +print(result) #output: success +``` +### Reset All Users Data Usage +```python +result = panel.reset_all_users_traffic(token=mytoken) +print(result) #output: success +``` +### Get All Users +```python +result = panel.get_all_users(token=mytoken) #return list of users +for user in result: + print(user.username) +``` +### Get User Usage +```python +result = panel.get_user_usage("mewhrzad",token=mytoken) +print(result) +#output: [{'node_id': None, 'node_name': 'MTN', 'used_traffic': 0}, +#{'node_id': 1, 'node_name': 'MCI', 'used_traffic': 0}] +``` +### Get All User Templates +```python +result = panel.get_all_templates(token=mytoken) #return template list object +for template in result: + print(template.name) +``` +### Add User Template +```python +from marzpy.api.template import Template + +temp = Template( + name="new_template", + inbounds={"vmess": ["VMESS TCP"], "vless": ["VLESS TCP REALITY"]}, + data_limit=0, + expire_duration=0, + username_prefix=None, + username_suffix=None, +) +result = panel.add_template(token=mytoken, template=temp) # return new Template object +print(result.name) #output: new_template +``` +### Get User Template +```python +template_id = 11 +result = panel.get_template_by_id(token=mytoken, id=template_id) # return Template object +print(result.name) #output: new_template +``` +### Modify User Template +```python +from marzpy.api.template import Template + +temp = Template( + name="new_template2", + inbounds={"vmess": ["VMESS TCP"], "vless": ["VLESS TCP REALITY"]}, + data_limit=0, + expire_duration=0, + username_prefix=None, + username_suffix=None, +) +result = panel.modify_template_by_id( + id=1, token=mytoken, template=temp) # return Modified Template object +print(result.name) #output: new_template2 +``` +### Remove User Template +```python +result = panel.delete_template_by_id(id=1, token=mytoken) +print(result) #output: success +``` +### Add Node +```python +from marzpy.api.node import Node + +my_node = Node( + name="somename", + address="test.example.com", + port=62050, + api_port=62051, + certificate="your_cert", + id=4, + xray_version="1.8.1", + status="connected", + message="string", +) + +result = panel.add_node(token=mytoken, node=my_node) # return new Node object +print(result.address) +``` +### Get Node +```python +result = panel.get_node_by_id(id=1, token=mytoken) # return exist Node object +print(result.address) #output: address of node 1 +``` +### Modify Node +```python +from marzpy.api.node import Node + +my_node = Node( + name="somename", + address="test.example.com", + port=62050, + api_port=62051, + certificate="your_cert", + id=4, + xray_version="1.8.1", + status="connected", + message="string", +) + +result = panel.modify_node_by_id(id=1, token=mytoken,node=my_node) # return modified Node object +print(result.address) #output:test.example.com +``` +### Remove Node +```python +result = panel.delete_node(id=1, token=mytoken) +print(result) #output: success +``` +### Get All Nodes +```python +result = panel.get_all_nodes(token=mytoken) # return List of Node object +for node in result: + print(node.address) +``` +### Reconenct Node +```python +result = panel.reconnect_node(id=1,token=mytoken) +print(result) #output: success +``` +### Get Node Usage +```python +result = panel.get_nodes_usage(token=mytoken) +for node in result: + print(node) +#output:{'node_id': 1, 'node_name': 'N1', 'uplink': 1000000000000, 'downlink': 1000000000000} +# {'node_id': 2, 'node_name': 'N2', 'uplink': 1000000000000, 'downlink': 1000000000000} +``` diff --git a/marzpy/__init__.py b/marzpy/__init__.py new file mode 100644 index 0000000..623df40 --- /dev/null +++ b/marzpy/__init__.py @@ -0,0 +1 @@ +from .marzban import Marzban diff --git a/marzpy/api/__init__.py b/marzpy/api/__init__.py new file mode 100644 index 0000000..b060bf6 --- /dev/null +++ b/marzpy/api/__init__.py @@ -0,0 +1,14 @@ +from .admin import Admin +from .node import NodeMethods +from .subscription import Subscription +from .core import Core +from .user import UserMethods +from .template import TemplateMethods +from .system import System + + +class Methods( + Admin, NodeMethods, Subscription, Core, UserMethods, TemplateMethods, System +): + def __init__(self, username: str, password: str, panel_address: str): + super().__init__(username, password, panel_address) diff --git a/marzpy/api/admin.py b/marzpy/api/admin.py new file mode 100644 index 0000000..8c1f939 --- /dev/null +++ b/marzpy/api/admin.py @@ -0,0 +1,99 @@ +from .send_requests import * +import aiohttp,json + +class Admin: + def __init__(self, username: str, password: str, panel_address: str): + self.username = username + self.password = password + self.panel_address = panel_address + + async def get_token(self): + """login for Authorization token + + Returns: `~dict`: Authorization token + """ + try: + async with aiohttp.request( + "post", + url = f"{self.panel_address}/api/admin/token", + data = {"username": self.username, "password": self.password}, + ) as response : + # response.raise_for_status() # Raise an exception for non-200 status codes + result = await response.json() + result["panel_address"] = self.panel_address + return result + except aiohttp.exceptions.RequestException as ex: + print(f"Request Exception: {ex}") + return None + except json.JSONDecodeError as ex: + print(f"JSON Decode Error: {ex}") + return None + + async def get_current_admin(self, token: dict): + """get current admin who has logged in. + + Parameters: + token (``dict``) : Authorization token + + Returns: + `~dict`: {"username": "str" , "is_sudo": true} + """ + return await send_request(endpoint="admin", token=token, method="get") + + async def create_admin(self, token: dict, data: dict): + """add new admin. + + Parameters: + token (``dict``) : Authorization token + data (``dict``) : information of new admin + + Returns: + `~dict`: username && is_sudo + """ + await send_request(endpoint="admin", token=token, method="post", data=data) + return "success" + + async def modify_admin(self, username: str, token: dict, data: dict): + """change exist admins password. + + *you cant modify sudo admins password* + + Parameters: + username (``str``) : username of admin + token (``dict``) : Authorization token + data (``dict``) : information of new admin + + Returns: + `~dict`: username && is_sudo + """ + await send_request( + endpoint=f"admin/{username}", + token=token, + method="put", + data=data, + ) + return "success" + + async def remove_admin(self, username: str, token: dict): + """delete admin. + + Parameters: + username (``str``) : username of admin + token (``dict``) : Authorization token + + Returns: + `~str`: success + """ + await send_request(endpoint=f"admin/{username}", token=token, method="delete") + return "success" + + async def get_all_admins(self, token: dict): + """get all admins. + + Parameters: + token (``dict``) : Authorization token + + Returns: + `~list`: [{username && is_sudo}] + """ + return await send_request(endpoint=f"admins", token=token, method="get") \ No newline at end of file diff --git a/marzpy/api/core.py b/marzpy/api/core.py new file mode 100644 index 0000000..c6b082e --- /dev/null +++ b/marzpy/api/core.py @@ -0,0 +1,53 @@ +from .send_requests import * + + +class Core: + def __init__(self) -> None: + pass + + async def get_xray_core(self, token: dict): + """get xray core. + + Parameters: + token (``dict``): Authorization token + + Returns: + `~dict`: xray core + """ + return await send_request(endpoint="core", token=token, method="get") + + async def restart_xray_core(self, token: dict): + """restart xray core. + + Parameters: + token (``dict``): Authorization token + + Returns: + `~str`: success + """ + await send_request(endpoint="core/restart", token=token, method="post") + return "success" + + async def get_xray_config(self, token: dict): + """get xray config. + + Parameters: + token (``dict``): Authorization token + + Returns: + `~dict`: xray config + """ + return await send_request(endpoint="core/config", token=token, method="get") + + async def modify_xray_config(self, token: dict, config: json): + """edit xray config. + + Parameters: + token (``dict``): Authorization token + config (``json``): json of new config + + Returns: + `~str`: success + """ + await send_request(endpoint="core/config", token=token, method="put", data=config) + return "success" \ No newline at end of file diff --git a/marzpy/api/node.py b/marzpy/api/node.py new file mode 100644 index 0000000..c61a706 --- /dev/null +++ b/marzpy/api/node.py @@ -0,0 +1,137 @@ +from .send_requests import * + + +class Node: + def __init__( + self, + name="", + address="", + port=0, + api_port=0, + certificate="", + id=0, + xray_version="", + status="", + message="", + ): + self.name = name + self.address = address + self.port = port + self.api_port = api_port + self.certificate = certificate + self.id = id + self.xray_version = xray_version + self.status = status + self.message = message + + +class NodeMethods: + def __init__(self) -> None: + pass + + async def add_node(self, token: dict, node: Node): + """add new node. + + Parameters: + token (``dict``): Authorization token + + node (``api.Node``): node object + + Returns: + `~object`: information of new node + """ + return Node( + **await send_request( + endpoint="node", token=token, method="post", data=node.__dict__ + ) + ) + + async def get_node_by_id(self, id: int, token: dict): + """get exist node from id. + + Parameters: + id (``int``): id of node + + token (``dict``): Authorization token + + Returns: + `~object`: information of new node + """ + return Node(**await send_request(endpoint=f"node/{id}", token=token, method="get")) + + async def modify_node_by_id(self, id: int, token: dict, node: object): + """edit exist node from id. + + Parameters: + id (``int``): id of node + + token (``dict``): Authorization token + + node (``api.Node``): node object + + Returns: + `~object`: information of new node + """ + request = await send_request( + endpoint=f"node/{id}", token=token, method="put", data=node.__dict__ + ) + return Node(**request) + + async def delete_node(self, id: int, token: dict): + """delete node from id. + + Parameters: + id (``int``): id of node + + token (``dict``): Authorization token + + Returns: + `~str`: success + """ + await send_request(endpoint=f"node/{id}", token=token, method="delete") + return "success" + + async def get_all_nodes(self, token: dict): + """get all nodes. + + Parameters: + token (``dict``): Authorization token + + Returns: + `~list of objects`: [Node] + """ + request = await send_request(endpoint="nodes", token=token, method="get") + node_list = [Node()] + for node in request: + node_list.append(Node(**node)) + del node_list[0] + return node_list + + async def reconnect_node(self, id: int, token: dict): + """reconnect from id. + + Parameters: + id (``int``): id of node + + token (``dict``): Authorization token + + Returns: + `~str`: success + """ + request = await send_request( + endpoint=f"node/{id}/reconnect", token=token, method="post" + ) + + return "success" + + async def get_nodes_usage(self, token: dict): + """get all nodes usage. + + Parameters: + token (``dict``): Authorization token + + Returns: + `~dict`: "usage" : [] + """ + request = await send_request(endpoint="nodes/usage", token=token, method="get") + return request["usages"] \ No newline at end of file diff --git a/marzpy/api/send_requests.py b/marzpy/api/send_requests.py new file mode 100644 index 0000000..5eb84c9 --- /dev/null +++ b/marzpy/api/send_requests.py @@ -0,0 +1,28 @@ +import aiohttp, json + +import aiohttp.client_exceptions +import aiohttp.http_exceptions + + +async def send_request(endpoint, token, method, data=None): + try: + print(json.dumps(data)) + panel_address = token["panel_address"] + token_type = token["token_type"] + access_token = token["access_token"] + request_address = f"{panel_address}/api/{endpoint}" + headers = { + "accept": "application/json", + "Authorization": f"{token_type} {access_token}", + "Content-Type": "application/json" + } + async with aiohttp.request( + method=method, + url=request_address, + headers=headers, + data=json.dumps(data), + raise_for_status=True, + ) as response : + return await response.json() + except json.JSONDecodeError as ex: + raise f"JSON Decode Error: {ex}" \ No newline at end of file diff --git a/marzpy/api/subscription.py b/marzpy/api/subscription.py new file mode 100644 index 0000000..051ab88 --- /dev/null +++ b/marzpy/api/subscription.py @@ -0,0 +1,34 @@ +import aiohttp, json, base64 + + +class Subscription: + def __init__(self) -> None: + pass + + async def subsend_request(sub_link: str, endpoint: str): + async with aiohttp.request( + method="get", + url = f"{sub_link}/{endpoint}", + headers={"Accept": "application/json"}, + raise_for_status=True, + ) as response : + result = await response.text() + if endpoint: + return await response.json() + else: + return base64.b64decode(result).decode("utf-8") + + async def get_subscription(self, sub_link: str): + """Unknow usage!""" + return await Subscription.subsend_request(sub_link, "") + + async def get_subscription_info(self, sub_link: str): + """get user information. + + Parameters: + token (``dict``): subscription token + + Returns: + `~dict`: information of user + """ + return await Subscription.subsend_request(sub_link, "info") \ No newline at end of file diff --git a/marzpy/api/system.py b/marzpy/api/system.py new file mode 100644 index 0000000..538e05d --- /dev/null +++ b/marzpy/api/system.py @@ -0,0 +1,50 @@ +from .send_requests import send_request + + +class System: + def __init__(self) -> None: + pass + + async def get_system_stats(self, token: dict): + """get server stats. + + Parameters: + token (``dict``): Authorization token + + Returns: + `~dict`: server stats + """ + return await send_request(endpoint="system", token=token, method="get") + + async def get_inbounds(self, token: dict): + """get server inbounds. + + Parameters: + token (``dict``): Authorization token + + Returns: + `~dict`: server inbounds + """ + return await send_request(endpoint="inbounds", token=token, method="get") + + async def get_hosts(self, token: dict): + """get server hosts. + + Parameters: + token (``dict``): Authorization token + + Returns: + `~dict`: server hosts + """ + return await send_request(endpoint="hosts", token=token, method="get") + + async def modify_hosts(self, token: dict, data: dict): + """get server hosts. + + Parameters: + token (``dict``): Authorization token + data (``dict``) : new hosts data + Returns: + `~dict`: server hosts + """ + return await send_request(endpoint="hosts", token=token, method="put", data=data) \ No newline at end of file diff --git a/marzpy/api/template.py b/marzpy/api/template.py new file mode 100644 index 0000000..e206d4a --- /dev/null +++ b/marzpy/api/template.py @@ -0,0 +1,99 @@ +from .send_requests import * + + +class Template: + def __init__( + self, + name="", + inbounds={}, + data_limit={}, + expire_duration=0, + username_prefix="", + username_suffix="", + id=None, + ): + self.name = name + self.inbounds = inbounds + self.data_limit = data_limit + self.expire_duration = expire_duration + self.username_prefix = username_prefix + self.username_suffix = username_suffix + self.id = id + + +class TemplateMethods: + async def get_all_templates(self, token: dict): + """get all templates list. + + Parameters: + token (``dict``) : Authorization token + + Returns: + `~list`: list of templates + """ + request = await send_request(endpoint="user_template", token=token, method="get") + template_list = [Template()] + for user in request: + template_list.append(Template(**user)) + del template_list[0] + return template_list + + async def add_template(self, template: Template, token: dict): + """add new template. + + Parameters: + token (``dict``) : Authorization token + template (``api.template object``) : template + + Returns: + `~object`: information of new template + """ + request = await send_request( + endpoint="user_template", token=token, method="post", data=template.__dict__ + ) + return Template(**request) + + async def get_template_by_id(self, id: int, token: dict): + """get exist template from id. + + Parameters: + id (``id``) : template id + token (``dict``) : Authorization token + Returns: + `~object`: information of template + """ + request = await send_request( + endpoint=f"user_template/{id}", token=token, method="get" + ) + + return Template(**request) + + async def modify_template_by_id(self, id: int, token: dict, template: Template): + """edit exist template from id. + + Parameters: + id (``id``) : template id + token (``dict``) : Authorization token + template (``object``) template + Returns: + `~object`: information of edited template + """ + request = await send_request( + endpoint=f"user_template/{id}", + token=token, + method="put", + data=template.__dict__, + ) + return Template(**request) + + async def delete_template_by_id(self, id: int, token: dict): + """delete template from id. + + Parameters: + id (``id``) : template id + token (``dict``) : Authorization token + Returns: + `~str`: success + """ + await send_request(endpoint=f"user_template/{id}", token=token, method="delete") + return "success" \ No newline at end of file diff --git a/marzpy/api/user.py b/marzpy/api/user.py new file mode 100644 index 0000000..f052b60 --- /dev/null +++ b/marzpy/api/user.py @@ -0,0 +1,201 @@ +from .send_requests import * + +async def delete_if_exist(dic,keys:list): + for key in keys: + if key in dic: + del dic[key] + return dic + +class User: + def __init__( + self, + username: str, + proxies: dict, + inbounds: dict, + data_limit: float, + data_limit_reset_strategy: str = "no_reset", + status="", + expire: float = 0, + used_traffic=0, + lifetime_used_traffic=0, + created_at="", + links=[], + subscription_url="", + excluded_inbounds={}, + note = "", + on_hold_timeout= 0, + on_hold_expire_duration = 0, + sub_updated_at = 0, + online_at = 0, + sub_last_user_agent:str = "" + ): + self.username = username + self.proxies = proxies + self.inbounds = inbounds + self.expire = expire + self.data_limit = data_limit + self.data_limit_reset_strategy = data_limit_reset_strategy + self.status = status + self.used_traffic = used_traffic + self.lifetime_used_traffic = lifetime_used_traffic + self.created_at = created_at + self.links = links + self.subscription_url = subscription_url + self.excluded_inbounds = excluded_inbounds + self.note = note + self.on_hold_timeout = on_hold_timeout + self.on_hold_expire_duration = on_hold_expire_duration + self.sub_last_user_agent = sub_last_user_agent + self.online_at = online_at + self.sub_updated_at = sub_updated_at +class UserMethods: + async def add_user(self, user: User, token: dict): + """add new user. + + Parameters: + user (``api.User``) : User Object + + token (``dict``) : Authorization token + + Returns: `~User`: api.User object + """ + user.status = "active" + if user.on_hold_expire_duration: + user.status = "on_hold" + request = await send_request( + endpoint="user", token=token, method="post", data=user.__dict__ + ) + return User(**request) + + async def get_user(self, user_username: str, token: dict): + """get exist user information by username. + + Parameters: + user_username (``str``) : username of user + + token (``dict``) : Authorization token + + Returns: `~User`: api.User object + """ + request = await send_request(f"user/{user_username}", token=token, method="get") + return User(**request) + + async def modify_user(self, user_username: str, token: dict, user: object): + """edit exist user by username. + + Parameters: + user_username (``str``) : username of user + + token (``dict``) : Authorization token + + user (``api.User``) : User Object + + Returns: `~User`: api.User object + """ + request = await send_request(f"user/{user_username}", token, "put", user.__dict__) + return User(**request) + + async def delete_user(self, user_username: str, token: dict): + """delete exist user by username. + + Parameters: + user_username (``str``) : username of user + + token (``dict``) : Authorization token + + Returns: `~str`: success + """ + await send_request(f"user/{user_username}", token, "delete") + return "success" + + async def reset_user_traffic(self, user_username: str, token: dict): + """reset exist user traffic by username. + + Parameters: + user_username (``str``) : username of user + + token (``dict``) : Authorization token + + Returns: `~str`: success + """ + await send_request(f"user/{user_username}/reset", token, "post") + return "success" + + async def revoke_sub(self, user_username: str, token: dict): + """Revoke users subscription (Subscription link and proxies) traffic by username. + + Parameters: + user_username (``str``) : username of user + + token (``dict``) : Authorization token + + Returns: `~str`: success + """ + request = await send_request(f"user/{user_username}/revoke_sub", token, "post") + return User(**request) + + async def get_all_users(self, token: dict, username=None, status=None): + """get all users list. + + Parameters: + token (``dict``) : Authorization token + + Returns: + `~list`: list of users + """ + endpoint = "users" + if username: + endpoint += f"?username={username}" + if status: + if "?" in endpoint: + endpoint += f"&status={status}" + else: + endpoint += f"?status={status}" + request = await send_request(endpoint, token, "get") + user_list = [ + User( + username="", + proxies={}, + inbounds={}, + expire=0, + data_limit=0, + data_limit_reset_strategy="", + ) + ] + for user in request["users"]: + user_list.append(User(**user)) + del user_list[0] + return user_list + + async def reset_all_users_traffic(self, token: dict): + """reset all users traffic. + + Parameters: + token (``dict``) : Authorization token + + Returns: `~str`: success + """ + await send_request("users/reset", token, "post") + return "success" + + async def get_user_usage(self, user_username: str, token: dict): + """get user usage by username. + + Parameters: + user_username (``str``) : username of user + + token (``dict``) : Authorization token + + Returns: `~dict`: dict of user usage + """ + return await end_request(f"user/{user_username}/usage", token, "get")["usages"] + + async def get_all_users_count(self, token: dict): + """get all users count. + + Parameters: + token (``dict``) : Authorization token + + Returns: `~int`: count of users + """ + return await self.get_all_users(token)["content"]["total"] \ No newline at end of file diff --git a/marzpy/marzban.py b/marzpy/marzban.py new file mode 100644 index 0000000..57fb587 --- /dev/null +++ b/marzpy/marzban.py @@ -0,0 +1,9 @@ +from .api import Methods + + +class Marzban(Methods): + def __init__(self, username: str, password: str, panel_address: str) -> None: + super().__init__(username, password, panel_address) + self.username = username + self.password = password + self.panel_address = panel_address diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..178cfce --- /dev/null +++ b/setup.py @@ -0,0 +1,17 @@ +from setuptools import setup, find_packages + +with open("README.md", "r") as file: + readme = file.read() + +setup( + name="marzpy", + version="0.0.4", + author="Mewhrzad", + description="a simple application with python to manage Marzban panel", + long_description="text/markdown", + url="https://github.com/Mewhrzad/marzpy", + keywords=["marzpy", "Marzban", "Gozargah", "Marzban python", "Marzban API"], + packages=find_packages(), + ins=["requests"], + classifiers=["Programming Language :: Python :: 3"], +) From 86bce8768aaf2a60221f7d224ce5ceb542960e30 Mon Sep 17 00:00:00 2001 From: mewhrzad Date: Sat, 13 Apr 2024 00:35:43 +0330 Subject: [PATCH 3/5] Async Frienly --- .github/workflows/python-package.yml | 1 + README.md | 66 ++++++++++++++-------------- 2 files changed, 34 insertions(+), 33 deletions(-) diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index fd04aae..e491f00 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -28,6 +28,7 @@ jobs: run: | python -m pip install --upgrade pip python -m pip install flake8 pytest + python -m pip install aiohttp if [ -f requirements.txt ]; then pip install -r requirements.txt; fi - name: Lint with flake8 run: | diff --git a/README.md b/README.md index ea69efd..828676a 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ A Python library that helps you easily use [Marzban](https://github.com/Gozargah ```shell pip install marzpy ``` -requirements : ```requests``` +requirements : ```aiohttp``` # How To Use ```python from marzpy import Marzban @@ -99,40 +99,40 @@ print(result) #output: success ### Remove Admin ```python target_admin = "test" -result = panel.remove_admin(username=target_admin,token=mytoken) +result = await panel.remove_admin(username=target_admin,token=mytoken) print(result) #output: success ``` ### Get All Admins ```python -result = panel.get_all_admins(token=mytoken) +result = await panel.get_all_admins(token=mytoken) print(result) #output: [{'username': 'test', 'is_sudo': True}, {'username': 'test1', 'is_sudo': False}] ``` ### User Subscription ```python subscription_url = "https://sub.yourdomain.com/sub/eyJhbGciOiJIUzI8NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJNbWRDcmFaeSIsImFjY2VzcyI8InN1YnNjcmlwdGlvbiIsImlhdCI1MTY5NDk1NTkxMH0.o75ML5835SPXpVPKXcvEIUxMTwSy-4XGS9NIdWOAmXY" -result = panel.get_subscription(subscription_url) +result = await panel.get_subscription(subscription_url) print(result) #output: Configs ``` ### User Subscription info ```python subscription_url = "https://sub.yourdomain.com/sub/eyJhbGciOiJIUzI8NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJNbWRDcmFaeSIsImFjY2VzcyI8InN1YnNjcmlwdGlvbiIsImlhdCI1MTY5NDk1NTkxMH0.o75ML5835SPXpVPKXcvEIUxMTwSy-4XGS9NIdWOAmXY" -result = panel.get_subscription_info(subscription_url) +result = await panel.get_subscription_info(subscription_url) print(result) #output: User information (usage,links,inbounds,....) ``` ### Get System Stats ```python -result = panel.get_system_stats(token=mytoken) +result = await panel.get_system_stats(token=mytoken) print(result) #output: system stats Memory & CPU usage ... ``` ### Get Inbounds ```python -result = panel.get_inbounds(token=mytoken) +result = await panel.get_inbounds(token=mytoken) print(result) #output: list of inbounds ``` ### Get Hosts ```python -result = panel.get_hosts(token=mytoken) +result = await panel.get_hosts(token=mytoken) print(result) #output: list of hosts ``` ### Modify Hosts @@ -152,30 +152,30 @@ hosts = { ] } # **Backup first** -result = panel.modify_hosts(token=mytoken,data=hosts) +result = await panel.modify_hosts(token=mytoken,data=hosts) print(result) #output: hosts ``` ### Get Core Stats ```python -result = panel.get_xray_core(token=mytoken) +result = await panel.get_xray_core(token=mytoken) print(result) #output: {'version': '1.8.1', 'started': True, 'logs_websocket': '/api/core/logs'} ``` ### Restart Core ```python -result = panel.restart_xray_core(token=mytoken) +result = await panel.restart_xray_core(token=mytoken) print(result) #output: success ``` ### Get Core Config ```python -result = panel.get_xray_config(token=mytoken) +result = await panel.get_xray_config(token=mytoken) print(result) #output: your xray core config ``` ### Modify Core Config ```python new_config={"your config"} -result = panel.modify_xray_config(token=mytoken,config=new_config) +result = await panel.modify_xray_config(token=mytoken,config=new_config) print(result) #output: success ``` ### Add User @@ -193,13 +193,13 @@ user = User( data_limit=0, data_limit_reset_strategy="no_reset", ) -result = panel.add_user(user=user, token=token) #return new User object +result = await panel.add_user(user=user, token=token) #return new User object print(result.username) #-> Mewhrzad, #user.proxies, #user.inbounds, #user.expire, #user.data_limit, #userdata_limit_reset_strategy, #user.status, #user.used_traffic, #user.lifetime_used_traffic, #user.created_at, #user.links, #user.subscription_url, #user.excluded_inbounds ``` ### Get User ```python -result = panel.get_user("Mewhrzad",token=mytoken) #return User object +result = await panel.get_user("Mewhrzad",token=mytoken) #return User object print(result.subscription_url) ``` ### Modify User @@ -216,40 +216,40 @@ new_user = User( data_limit_reset_strategy="no_reset", status="active", ) -result = panel.modify_user("Mewhrzad", token=mytoken, user=new_user) +result = await panel.modify_user("Mewhrzad", token=mytoken, user=new_user) print(result.subscription_url) #output: modified user object ``` ### Remove User ```python -result = panel.delete_user("test", token=mytoken) +result = await panel.delete_user("test", token=mytoken) print(result) #output: success ``` ### Reset User Data Usage ```python -result = panel.reset_user_traffic("test", token=mytoken) +result = await panel.reset_user_traffic("test", token=mytoken) print(result) #output: success ``` ### Reset All Users Data Usage ```python -result = panel.reset_all_users_traffic(token=mytoken) +result = await panel.reset_all_users_traffic(token=mytoken) print(result) #output: success ``` ### Get All Users ```python -result = panel.get_all_users(token=mytoken) #return list of users +result = await panel.get_all_users(token=mytoken) #return list of users for user in result: print(user.username) ``` ### Get User Usage ```python -result = panel.get_user_usage("mewhrzad",token=mytoken) +result = await panel.get_user_usage("mewhrzad",token=mytoken) print(result) #output: [{'node_id': None, 'node_name': 'MTN', 'used_traffic': 0}, #{'node_id': 1, 'node_name': 'MCI', 'used_traffic': 0}] ``` ### Get All User Templates ```python -result = panel.get_all_templates(token=mytoken) #return template list object +result = await panel.get_all_templates(token=mytoken) #return template list object for template in result: print(template.name) ``` @@ -265,13 +265,13 @@ temp = Template( username_prefix=None, username_suffix=None, ) -result = panel.add_template(token=mytoken, template=temp) # return new Template object +result = await panel.add_template(token=mytoken, template=temp) # return new Template object print(result.name) #output: new_template ``` ### Get User Template ```python template_id = 11 -result = panel.get_template_by_id(token=mytoken, id=template_id) # return Template object +result = await panel.get_template_by_id(token=mytoken, id=template_id) # return Template object print(result.name) #output: new_template ``` ### Modify User Template @@ -286,13 +286,13 @@ temp = Template( username_prefix=None, username_suffix=None, ) -result = panel.modify_template_by_id( +result = await panel.modify_template_by_id( id=1, token=mytoken, template=temp) # return Modified Template object print(result.name) #output: new_template2 ``` ### Remove User Template ```python -result = panel.delete_template_by_id(id=1, token=mytoken) +result = await panel.delete_template_by_id(id=1, token=mytoken) print(result) #output: success ``` ### Add Node @@ -311,12 +311,12 @@ my_node = Node( message="string", ) -result = panel.add_node(token=mytoken, node=my_node) # return new Node object +result = await panel.add_node(token=mytoken, node=my_node) # return new Node object print(result.address) ``` ### Get Node ```python -result = panel.get_node_by_id(id=1, token=mytoken) # return exist Node object +result = await panel.get_node_by_id(id=1, token=mytoken) # return exist Node object print(result.address) #output: address of node 1 ``` ### Modify Node @@ -335,28 +335,28 @@ my_node = Node( message="string", ) -result = panel.modify_node_by_id(id=1, token=mytoken,node=my_node) # return modified Node object +result = await panel.modify_node_by_id(id=1, token=mytoken,node=my_node) # return modified Node object print(result.address) #output:test.example.com ``` ### Remove Node ```python -result = panel.delete_node(id=1, token=mytoken) +result = await panel.delete_node(id=1, token=mytoken) print(result) #output: success ``` ### Get All Nodes ```python -result = panel.get_all_nodes(token=mytoken) # return List of Node object +result = await panel.get_all_nodes(token=mytoken) # return List of Node object for node in result: print(node.address) ``` ### Reconenct Node ```python -result = panel.reconnect_node(id=1,token=mytoken) +result = await panel.reconnect_node(id=1,token=mytoken) print(result) #output: success ``` ### Get Node Usage ```python -result = panel.get_nodes_usage(token=mytoken) +result = await panel.get_nodes_usage(token=mytoken) for node in result: print(node) #output:{'node_id': 1, 'node_name': 'N1', 'uplink': 1000000000000, 'downlink': 1000000000000} From ac8ab3ab124a5f5e2bc327ffbbf1b5629342c6b6 Mon Sep 17 00:00:00 2001 From: mewhrzad Date: Sat, 13 Apr 2024 00:55:06 +0330 Subject: [PATCH 4/5] Async Friendly --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 178cfce..36d3dd9 100644 --- a/setup.py +++ b/setup.py @@ -12,6 +12,6 @@ url="https://github.com/Mewhrzad/marzpy", keywords=["marzpy", "Marzban", "Gozargah", "Marzban python", "Marzban API"], packages=find_packages(), - ins=["requests"], + ins=["aiohttp"], classifiers=["Programming Language :: Python :: 3"], ) From 3de52f2427a1c23110effa012ddd9ebb7c425fad Mon Sep 17 00:00:00 2001 From: mewhrzad Date: Sun, 14 Apr 2024 17:07:14 +0330 Subject: [PATCH 5/5] Async Friendly --- .github/workflows/python-publish.yml | 41 +++ .gitignore | 161 ++++++++++++ LICENSE | 21 ++ README.md | 367 +++++++++++++++++++++++++++ marzpy/__init__.py | 1 + marzpy/api/admin.py | 99 ++++++++ marzpy/api/core.py | 53 ++++ marzpy/api/node.py | 137 ++++++++++ marzpy/api/send_requests.py | 29 +++ marzpy/api/subscription.py | 38 +++ marzpy/api/system.py | 50 ++++ marzpy/api/template.py | 99 ++++++++ marzpy/api/user.py | 201 +++++++++++++++ marzpy/marzban.py | 9 + setup.py | 17 ++ 15 files changed, 1323 insertions(+) create mode 100644 .github/workflows/python-publish.yml create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 README.md create mode 100644 marzpy/__init__.py create mode 100644 marzpy/api/admin.py create mode 100644 marzpy/api/core.py create mode 100644 marzpy/api/node.py create mode 100644 marzpy/api/send_requests.py create mode 100644 marzpy/api/subscription.py create mode 100644 marzpy/api/system.py create mode 100644 marzpy/api/template.py create mode 100644 marzpy/api/user.py create mode 100644 marzpy/marzban.py create mode 100644 setup.py diff --git a/.github/workflows/python-publish.yml b/.github/workflows/python-publish.yml new file mode 100644 index 0000000..88cfffb --- /dev/null +++ b/.github/workflows/python-publish.yml @@ -0,0 +1,41 @@ +# This workflow will upload a Python Package using Twine when a release is created +# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python#publishing-to-package-registries + +# This workflow uses actions that are not certified by GitHub. +# They are provided by a third-party and are governed by +# separate terms of service, privacy policy, and support +# documentation. + +name: Upload Python Package + +on: + release: + types: [created] + workflow_dispatch: + +permissions: + contents: read + +jobs: + deploy: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + - name: Set up Python + uses: actions/setup-python@v3 + with: + python-version: '3.x' + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install build + pip install aiohttp + - name: Build package + run: python -m build + - name: Publish package + uses: pypa/gh-action-pypi-publish@27b31702a0e7fc50959f5ad993c78deac1bdfc29 + with: + user: ${{ secrets.PYPI_USERNAME }} + password: ${{ secrets.PYPI_TOKEN }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..462f787 --- /dev/null +++ b/.gitignore @@ -0,0 +1,161 @@ +eg.py +.vscode +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/#use-with-ide +.pdm.toml + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..f6d3698 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 Mehrzad Khosravi + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..177f698 --- /dev/null +++ b/README.md @@ -0,0 +1,367 @@ +# marzpy +A Python library that helps you easily use [Marzban](https://github.com/Gozargah/Marzban)'s API panel +>[!IMPORTANT] +>**Status:** Working on new update 🔥 +## installation +```shell +pip install marzpy +``` +requirements : ```aiohttp``` +# How To Use +```python +from marzpy import Marzban +import asyncio + +async def main(): + panel = Marzban("username","password","https://example.com") + token = await await panel.get_token() + #await await panel.anyfunction(token) + +asyncio.run(main()) + +``` +# Features + +- Admin + - [get token](#get-token) + - [get admin](#get-current-admin) + - [create admin](#create-admin) + - [modify admin](#modify-admin) + - [remove admin](#remove-admin) + - [get all admins](#get-all-admins) +- Subscription + - [user subscription](#user-subscription) + - [user subscription info](#user-subscription-info) +- System + - [get system stats](#get-system-stats) + - [get inbounds](#get-inbounds) + - [get hosts](#get-hosts) + - [modify hosts](#modify-hosts) +- Core + - [get core stats](#get-core-stats) + - [restart core](#restart-core) + - [get core config](#get-core-config) + - [modify core config](#modify-core-config) +- User + - [add user](#add-user) + - [get user](#get-user) + - [modify user](#modify-user) + - [remove user](#remove-user) + - [reset user data usage](#reset-user-data-usage) + - [reset all users data usage](#reset-all-users-data-usage) + - [get all users](#get-all-users) + - [get user usage](#get-user-usage) +- User Template + - [get all user templates](#get-all-user-templates) + - [add user template](#add-user-template) + - [get user template](#get-user-template) + - [modify user template](#modify-user-template) + - [remove user template](#remove-user-template) +- Node + - [add node](#add-node) + - [get node](#get-node) + - [modify node](#modify-node) + - [remove node](#remove-node) + - [get all nodes](#get-all-nodes) + - [reconenct node](#reconenct-node) + - [get all nodes usage](#get-node-usage) + - +## Thanks To + +- [ErfanTech](https://github.com/ErfanTech) :laughing: + +# Examples +### Get Token +```python + +from marzpy import Marzban + +panel = Marzban("username","password","https://example.com") + +mytoken = await panel.get_token() + +``` +### Get Current admin +```python +admin = await panel.get_current_admin(token=mytoken) +print(admin) #output: {'username': 'admin', 'is_sudo': True} +``` +### Create Admin +```python +info = {'username':'test','password':'pasword','is_sudo':False} +rsault = await panel.create_admin(token=mytoken,data=info) +print(result) #output: success +``` +### Modify Admin +```python +target_admin = "test" +info = {'password':'newpassword','is_sudo':False} +result = await panel.change_admin_password(username=target_admin,token=mytoken,data=info) +print(result) #output: success +``` +### Remove Admin +```python +target_admin = "test" +result = await panel.delete_admin(username=target_admin,token=mytoken) +print(result) #output: success +``` +### Get All Admins +```python +result = await panel.get_all_admins(token=mytoken) +print(result) +#output: [{'username': 'test', 'is_sudo': True}, {'username': 'test1', 'is_sudo': False}] +``` +### User Subscription +```python +subscription_url = "https://sub.yourdomain.com/sub/eyJhbGciOiJIUzI8NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJNbWRDcmFaeSIsImFjY2VzcyI8InN1YnNjcmlwdGlvbiIsImlhdCI1MTY5NDk1NTkxMH0.o75ML5835SPXpVPKXcvEIUxMTwSy-4XGS9NIdWOAmXY" +result = await panel.get_subscription(subscription_url) +print(result) #output: Configs +``` +### User Subscription info +```python +subscription_url = "https://sub.yourdomain.com/sub/eyJhbGciOiJIUzI8NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJNbWRDcmFaeSIsImFjY2VzcyI8InN1YnNjcmlwdGlvbiIsImlhdCI1MTY5NDk1NTkxMH0.o75ML5835SPXpVPKXcvEIUxMTwSy-4XGS9NIdWOAmXY" +result = await panel.get_subscription_info(subscription_url) +print(result) #output: User information (usage,links,inbounds,....) +``` +### Get System Stats +```python +result = await panel.get_system_stats(token=mytoken) +print(result) #output: system stats Memory & CPU usage ... +``` +### Get Inbounds +```python +result = await panel.get_inbounds(token=mytoken) +print(result) #output: list of inbounds +``` +### Get Hosts +```python +result = await panel.get_hosts(token=mytoken) +print(result) #output: list of hosts +``` +### Modify Hosts +```python +hosts = { + "VMess TCP": [ + { + "remark": "somename", + "address": "someaddress", + "port": 0, + "sni": "somesni", + "host": "somehost", + "security": "inbound_default", + "alpn": "", + "fingerprint": "" + } + ] +} +# **Backup first** +result = await panel.modify_hosts(token=mytoken,data=hosts) +print(result) #output: hosts +``` +### Get Core Stats +```python +result = await panel.get_xray_core(token=mytoken) +print(result) + #output: {'version': '1.8.1', 'started': True, 'logs_websocket': '/api/core/logs'} +``` +### Restart Core +```python +result = await panel.restart_xray_core(token=mytoken) +print(result) + #output: success +``` +### Get Core Config +```python +result = await panel.get_xray_config(token=mytoken) +print(result) #output: your xray core config +``` +### Modify Core Config +```python +new_config={"your config"} +result = await panel.modify_xray_config(token=mytoken,config=new_config) +print(result) #output: success +``` +### Add User +```python +from marzpy.api.user import User + +user = User( + username="Mewhrzad", + proxies={ + "vmess": {"id": "35e7e39c-7d5c-1f4b-8b71-508e4f37ff53"}, + "vless": {"id": "35e7e39c-7d5c-1f4b-8b71-508e4f37ff53"}, + }, + inbounds={"vmess": ["VMess TCP"], "vless": ["VLESS TCP REALITY"]}, + expire=0, + data_limit=0, + data_limit_reset_strategy="no_reset", +) +result = await panel.add_user(user=user, token=token) #return new User object + +print(result.username) #-> Mewhrzad, #user.proxies, #user.inbounds, #user.expire, #user.data_limit, #userdata_limit_reset_strategy, #user.status, #user.used_traffic, #user.lifetime_used_traffic, #user.created_at, #user.links, #user.subscription_url, #user.excluded_inbounds +``` +### Get User +```python +result = await panel.get_user("Mewhrzad",token=mytoken) #return User object +print(result.subscription_url) +``` +### Modify User +```python +new_user = User( + username="test", + proxies={ + "vmess": {"id": "35e4e39c-7d5c-4f4b-8b71-558e4f37ff53"}, + "vless": {"id": "35e4e39c-7d5c-4f4b-8b71-558e4f37ff53"}, + }, + inbounds={"vmess": ["VMess TCP"], "vless": ["VLESS TCP REALITY"]}, + expire=0, + data_limit=0, + data_limit_reset_strategy="no_reset", + status="active", +) +result = await panel.modify_user("Mewhrzad", token=mytoken, user=new_user) +print(result.subscription_url) #output: modified user object +``` +### Remove User +```python +result = await panel.delete_user("test", token=mytoken) +print(result) #output: success +``` +### Reset User Data Usage +```python +result = await panel.reset_user_traffic("test", token=mytoken) +print(result) #output: success +``` +### Reset All Users Data Usage +```python +result = await panel.reset_all_users_traffic(token=mytoken) +print(result) #output: success +``` +### Get All Users +```python +result = await panel.get_all_users(token=mytoken) #return list of users +for user in result: + print(user.username) +``` +### Get User Usage +```python +result = await panel.get_user_usage("mewhrzad",token=mytoken) +print(result) +#output: [{'node_id': None, 'node_name': 'MTN', 'used_traffic': 0}, +#{'node_id': 1, 'node_name': 'MCI', 'used_traffic': 0}] +``` +### Get All User Templates +```python +result = await panel.get_all_templates(token=mytoken) #return template list object +for template in result: + print(template.name) +``` +### Add User Template +```python +from marzpy.api.template import Template + +temp = Template( + name="new_template", + inbounds={"vmess": ["VMESS TCP"], "vless": ["VLESS TCP REALITY"]}, + data_limit=0, + expire_duration=0, + username_prefix=None, + username_suffix=None, +) +result = await panel.add_template(token=mytoken, template=temp) # return new Template object +print(result.name) #output: new_template +``` +### Get User Template +```python +template_id = 11 +result = await panel.get_template_by_id(token=mytoken, id=template_id) # return Template object +print(result.name) #output: new_template +``` +### Modify User Template +```python +from marzpy.api.template import Template + +temp = Template( + name="new_template2", + inbounds={"vmess": ["VMESS TCP"], "vless": ["VLESS TCP REALITY"]}, + data_limit=0, + expire_duration=0, + username_prefix=None, + username_suffix=None, +) +result = await panel.modify_template_by_id( + id=1, token=mytoken, template=temp) # return Modified Template object +print(result.name) #output: new_template2 +``` +### Remove User Template +```python +result = await panel.delete_template_by_id(id=1, token=mytoken) +print(result) #output: success +``` +### Add Node +```python +from marzpy.api.node import Node + +my_node = Node( + name="somename", + address="test.example.com", + port=62050, + api_port=62051, + certificate="your_cert", + id=4, + xray_version="1.8.1", + status="connected", + message="string", +) + +result = await panel.add_node(token=mytoken, node=my_node) # return new Node object +print(result.address) +``` +### Get Node +```python +result = await panel.get_node_by_id(id=1, token=mytoken) # return exist Node object +print(result.address) #output: address of node 1 +``` +### Modify Node +```python +from marzpy.api.node import Node + +my_node = Node( + name="somename", + address="test.example.com", + port=62050, + api_port=62051, + certificate="your_cert", + id=4, + xray_version="1.8.1", + status="connected", + message="string", +) + +result = await panel.modify_node_by_id(id=1, token=mytoken,node=my_node) # return modified Node object +print(result.address) #output:test.example.com +``` +### Remove Node +```python +result = await panel.delete_node(id=1, token=mytoken) +print(result) #output: success +``` +### Get All Nodes +```python +result = await panel.get_all_nodes(token=mytoken) # return List of Node object +for node in result: + print(node.address) +``` +### Reconenct Node +```python +result = await panel.reconnect_node(id=1,token=mytoken) +print(result) #output: success +``` +### Get Node Usage +```python +result = await panel.get_nodes_usage(token=mytoken) +for node in result: + print(node) +#output:{'node_id': 1, 'node_name': 'N1', 'uplink': 1000000000000, 'downlink': 1000000000000} +# {'node_id': 2, 'node_name': 'N2', 'uplink': 1000000000000, 'downlink': 1000000000000} +``` \ No newline at end of file diff --git a/marzpy/__init__.py b/marzpy/__init__.py new file mode 100644 index 0000000..623df40 --- /dev/null +++ b/marzpy/__init__.py @@ -0,0 +1 @@ +from .marzban import Marzban diff --git a/marzpy/api/admin.py b/marzpy/api/admin.py new file mode 100644 index 0000000..a362125 --- /dev/null +++ b/marzpy/api/admin.py @@ -0,0 +1,99 @@ +from .send_requests import * +import aiohttp,json + +class Admin: + def __init__(self, username: str, password: str, panel_address: str): + self.username = username + self.password = password + self.panel_address = panel_address + + async def get_token(self): + """login for Authorization token + + Returns: `~dict`: Authorization token + """ + try: + async with aiohttp.request( + "post", + url = f"{self.panel_address}/api/admin/token", + data = {"username": self.username, "password": self.password}, + ) as response : + # response.raise_for_status() # Raise an exception for non-200 status codes + result = await response.json() + result["panel_address"] = self.panel_address + return result + except aiohttp.exceptions.RequestException as ex: + print(f"Request Exception: {ex}") + return None + except json.JSONDecodeError as ex: + print(f"JSON Decode Error: {ex}") + return None + + async def get_current_admin(self, token: dict): + """get current admin who has logged in. + + Parameters: + token (``dict``) : Authorization token + + Returns: + `~dict`: {"username": "str" , "is_sudo": true} + """ + return await send_request(endpoint="admin", token=token, method="get") + + async def create_admin(self, token: dict, data: dict): + """add new admin. + + Parameters: + token (``dict``) : Authorization token + data (``dict``) : information of new admin + + Returns: + `~dict`: username && is_sudo + """ + await send_request(endpoint="admin", token=token, method="post", data=data) + return "success" + + async def change_admin_password(self, username: str, token: dict, data: dict): + """change exist admins password. + + *you cant modify sudo admins password* + + Parameters: + username (``str``) : username of admin + token (``dict``) : Authorization token + data (``dict``) : information of new admin + + Returns: + `~dict`: username && is_sudo + """ + await send_request( + endpoint=f"admin/{username}", + token=token, + method="put", + data=data, + ) + return "success" + + async def delete_admin(self, username: str, token: dict): + """delete admin. + + Parameters: + username (``str``) : username of admin + token (``dict``) : Authorization token + + Returns: + `~str`: success + """ + await send_request(endpoint=f"admin/{username}", token=token, method="delete") + return "success" + + async def get_all_admins(self, token: dict): + """get all admins. + + Parameters: + token (``dict``) : Authorization token + + Returns: + `~list`: [{username && is_sudo}] + """ + return await send_request(endpoint=f"admins", token=token, method="get") \ No newline at end of file diff --git a/marzpy/api/core.py b/marzpy/api/core.py new file mode 100644 index 0000000..c6b082e --- /dev/null +++ b/marzpy/api/core.py @@ -0,0 +1,53 @@ +from .send_requests import * + + +class Core: + def __init__(self) -> None: + pass + + async def get_xray_core(self, token: dict): + """get xray core. + + Parameters: + token (``dict``): Authorization token + + Returns: + `~dict`: xray core + """ + return await send_request(endpoint="core", token=token, method="get") + + async def restart_xray_core(self, token: dict): + """restart xray core. + + Parameters: + token (``dict``): Authorization token + + Returns: + `~str`: success + """ + await send_request(endpoint="core/restart", token=token, method="post") + return "success" + + async def get_xray_config(self, token: dict): + """get xray config. + + Parameters: + token (``dict``): Authorization token + + Returns: + `~dict`: xray config + """ + return await send_request(endpoint="core/config", token=token, method="get") + + async def modify_xray_config(self, token: dict, config: json): + """edit xray config. + + Parameters: + token (``dict``): Authorization token + config (``json``): json of new config + + Returns: + `~str`: success + """ + await send_request(endpoint="core/config", token=token, method="put", data=config) + return "success" \ No newline at end of file diff --git a/marzpy/api/node.py b/marzpy/api/node.py new file mode 100644 index 0000000..c61a706 --- /dev/null +++ b/marzpy/api/node.py @@ -0,0 +1,137 @@ +from .send_requests import * + + +class Node: + def __init__( + self, + name="", + address="", + port=0, + api_port=0, + certificate="", + id=0, + xray_version="", + status="", + message="", + ): + self.name = name + self.address = address + self.port = port + self.api_port = api_port + self.certificate = certificate + self.id = id + self.xray_version = xray_version + self.status = status + self.message = message + + +class NodeMethods: + def __init__(self) -> None: + pass + + async def add_node(self, token: dict, node: Node): + """add new node. + + Parameters: + token (``dict``): Authorization token + + node (``api.Node``): node object + + Returns: + `~object`: information of new node + """ + return Node( + **await send_request( + endpoint="node", token=token, method="post", data=node.__dict__ + ) + ) + + async def get_node_by_id(self, id: int, token: dict): + """get exist node from id. + + Parameters: + id (``int``): id of node + + token (``dict``): Authorization token + + Returns: + `~object`: information of new node + """ + return Node(**await send_request(endpoint=f"node/{id}", token=token, method="get")) + + async def modify_node_by_id(self, id: int, token: dict, node: object): + """edit exist node from id. + + Parameters: + id (``int``): id of node + + token (``dict``): Authorization token + + node (``api.Node``): node object + + Returns: + `~object`: information of new node + """ + request = await send_request( + endpoint=f"node/{id}", token=token, method="put", data=node.__dict__ + ) + return Node(**request) + + async def delete_node(self, id: int, token: dict): + """delete node from id. + + Parameters: + id (``int``): id of node + + token (``dict``): Authorization token + + Returns: + `~str`: success + """ + await send_request(endpoint=f"node/{id}", token=token, method="delete") + return "success" + + async def get_all_nodes(self, token: dict): + """get all nodes. + + Parameters: + token (``dict``): Authorization token + + Returns: + `~list of objects`: [Node] + """ + request = await send_request(endpoint="nodes", token=token, method="get") + node_list = [Node()] + for node in request: + node_list.append(Node(**node)) + del node_list[0] + return node_list + + async def reconnect_node(self, id: int, token: dict): + """reconnect from id. + + Parameters: + id (``int``): id of node + + token (``dict``): Authorization token + + Returns: + `~str`: success + """ + request = await send_request( + endpoint=f"node/{id}/reconnect", token=token, method="post" + ) + + return "success" + + async def get_nodes_usage(self, token: dict): + """get all nodes usage. + + Parameters: + token (``dict``): Authorization token + + Returns: + `~dict`: "usage" : [] + """ + request = await send_request(endpoint="nodes/usage", token=token, method="get") + return request["usages"] \ No newline at end of file diff --git a/marzpy/api/send_requests.py b/marzpy/api/send_requests.py new file mode 100644 index 0000000..dd583ce --- /dev/null +++ b/marzpy/api/send_requests.py @@ -0,0 +1,29 @@ +import aiohttp, json + + +async def send_request(endpoint, token, method, data=None): + try: + panel_address = token["panel_address"] + token_type = token["token_type"] + access_token = token["access_token"] + request_address = f"{panel_address}/api/{endpoint}" + headers = { + "accept": "application/json", + "Authorization": f"{token_type} {access_token}", + } + async with aiohttp.request( + method=method, + url=request_address, + headers=headers, + data=json.dumps(data) + ) as response : + response.raise_for_status() # Raise an exception for non-200 status codes + result = await response.json() + return result + except aiohttp.exceptions.RequestException as ex: + if response.content: + raise Exception(f"Request Exception: { await response.content }") + else: + raise ex + except json.JSONDecodeError as ex: + raise f"JSON Decode Error: {ex}" \ No newline at end of file diff --git a/marzpy/api/subscription.py b/marzpy/api/subscription.py new file mode 100644 index 0000000..5901deb --- /dev/null +++ b/marzpy/api/subscription.py @@ -0,0 +1,38 @@ +import aiohttp, json, base64 + + +class Subscription: + def __init__(self) -> None: + pass + + async def subsend_request(sub_link: str, endpoint: str): + try: + async with aiohttp.request( + method="get", + url = f"{sub_link}/{endpoint}", + headers={"Accept": "application/json"} + ) as response : + await response.raise_for_status() # Raise an exception for non-200 status codes + result = await response.content + if endpoint: + return await response.json + else: + return base64.b64decode(result).decode("utf-8") + except aiohttp.exceptions.RequestException as ex: + print(f"Request Exception: {ex}") + return None + + async def get_subscription(self, sub_link: str): + """Unknow usage!""" + return await Subscription.subsend_request(sub_link, "") + + async def get_subscription_info(self, sub_link: str): + """get user information. + + Parameters: + token (``dict``): subscription token + + Returns: + `~dict`: information of user + """ + return await Subscription.subsend_request(sub_link, "info") \ No newline at end of file diff --git a/marzpy/api/system.py b/marzpy/api/system.py new file mode 100644 index 0000000..538e05d --- /dev/null +++ b/marzpy/api/system.py @@ -0,0 +1,50 @@ +from .send_requests import send_request + + +class System: + def __init__(self) -> None: + pass + + async def get_system_stats(self, token: dict): + """get server stats. + + Parameters: + token (``dict``): Authorization token + + Returns: + `~dict`: server stats + """ + return await send_request(endpoint="system", token=token, method="get") + + async def get_inbounds(self, token: dict): + """get server inbounds. + + Parameters: + token (``dict``): Authorization token + + Returns: + `~dict`: server inbounds + """ + return await send_request(endpoint="inbounds", token=token, method="get") + + async def get_hosts(self, token: dict): + """get server hosts. + + Parameters: + token (``dict``): Authorization token + + Returns: + `~dict`: server hosts + """ + return await send_request(endpoint="hosts", token=token, method="get") + + async def modify_hosts(self, token: dict, data: dict): + """get server hosts. + + Parameters: + token (``dict``): Authorization token + data (``dict``) : new hosts data + Returns: + `~dict`: server hosts + """ + return await send_request(endpoint="hosts", token=token, method="put", data=data) \ No newline at end of file diff --git a/marzpy/api/template.py b/marzpy/api/template.py new file mode 100644 index 0000000..e206d4a --- /dev/null +++ b/marzpy/api/template.py @@ -0,0 +1,99 @@ +from .send_requests import * + + +class Template: + def __init__( + self, + name="", + inbounds={}, + data_limit={}, + expire_duration=0, + username_prefix="", + username_suffix="", + id=None, + ): + self.name = name + self.inbounds = inbounds + self.data_limit = data_limit + self.expire_duration = expire_duration + self.username_prefix = username_prefix + self.username_suffix = username_suffix + self.id = id + + +class TemplateMethods: + async def get_all_templates(self, token: dict): + """get all templates list. + + Parameters: + token (``dict``) : Authorization token + + Returns: + `~list`: list of templates + """ + request = await send_request(endpoint="user_template", token=token, method="get") + template_list = [Template()] + for user in request: + template_list.append(Template(**user)) + del template_list[0] + return template_list + + async def add_template(self, template: Template, token: dict): + """add new template. + + Parameters: + token (``dict``) : Authorization token + template (``api.template object``) : template + + Returns: + `~object`: information of new template + """ + request = await send_request( + endpoint="user_template", token=token, method="post", data=template.__dict__ + ) + return Template(**request) + + async def get_template_by_id(self, id: int, token: dict): + """get exist template from id. + + Parameters: + id (``id``) : template id + token (``dict``) : Authorization token + Returns: + `~object`: information of template + """ + request = await send_request( + endpoint=f"user_template/{id}", token=token, method="get" + ) + + return Template(**request) + + async def modify_template_by_id(self, id: int, token: dict, template: Template): + """edit exist template from id. + + Parameters: + id (``id``) : template id + token (``dict``) : Authorization token + template (``object``) template + Returns: + `~object`: information of edited template + """ + request = await send_request( + endpoint=f"user_template/{id}", + token=token, + method="put", + data=template.__dict__, + ) + return Template(**request) + + async def delete_template_by_id(self, id: int, token: dict): + """delete template from id. + + Parameters: + id (``id``) : template id + token (``dict``) : Authorization token + Returns: + `~str`: success + """ + await send_request(endpoint=f"user_template/{id}", token=token, method="delete") + return "success" \ No newline at end of file diff --git a/marzpy/api/user.py b/marzpy/api/user.py new file mode 100644 index 0000000..f052b60 --- /dev/null +++ b/marzpy/api/user.py @@ -0,0 +1,201 @@ +from .send_requests import * + +async def delete_if_exist(dic,keys:list): + for key in keys: + if key in dic: + del dic[key] + return dic + +class User: + def __init__( + self, + username: str, + proxies: dict, + inbounds: dict, + data_limit: float, + data_limit_reset_strategy: str = "no_reset", + status="", + expire: float = 0, + used_traffic=0, + lifetime_used_traffic=0, + created_at="", + links=[], + subscription_url="", + excluded_inbounds={}, + note = "", + on_hold_timeout= 0, + on_hold_expire_duration = 0, + sub_updated_at = 0, + online_at = 0, + sub_last_user_agent:str = "" + ): + self.username = username + self.proxies = proxies + self.inbounds = inbounds + self.expire = expire + self.data_limit = data_limit + self.data_limit_reset_strategy = data_limit_reset_strategy + self.status = status + self.used_traffic = used_traffic + self.lifetime_used_traffic = lifetime_used_traffic + self.created_at = created_at + self.links = links + self.subscription_url = subscription_url + self.excluded_inbounds = excluded_inbounds + self.note = note + self.on_hold_timeout = on_hold_timeout + self.on_hold_expire_duration = on_hold_expire_duration + self.sub_last_user_agent = sub_last_user_agent + self.online_at = online_at + self.sub_updated_at = sub_updated_at +class UserMethods: + async def add_user(self, user: User, token: dict): + """add new user. + + Parameters: + user (``api.User``) : User Object + + token (``dict``) : Authorization token + + Returns: `~User`: api.User object + """ + user.status = "active" + if user.on_hold_expire_duration: + user.status = "on_hold" + request = await send_request( + endpoint="user", token=token, method="post", data=user.__dict__ + ) + return User(**request) + + async def get_user(self, user_username: str, token: dict): + """get exist user information by username. + + Parameters: + user_username (``str``) : username of user + + token (``dict``) : Authorization token + + Returns: `~User`: api.User object + """ + request = await send_request(f"user/{user_username}", token=token, method="get") + return User(**request) + + async def modify_user(self, user_username: str, token: dict, user: object): + """edit exist user by username. + + Parameters: + user_username (``str``) : username of user + + token (``dict``) : Authorization token + + user (``api.User``) : User Object + + Returns: `~User`: api.User object + """ + request = await send_request(f"user/{user_username}", token, "put", user.__dict__) + return User(**request) + + async def delete_user(self, user_username: str, token: dict): + """delete exist user by username. + + Parameters: + user_username (``str``) : username of user + + token (``dict``) : Authorization token + + Returns: `~str`: success + """ + await send_request(f"user/{user_username}", token, "delete") + return "success" + + async def reset_user_traffic(self, user_username: str, token: dict): + """reset exist user traffic by username. + + Parameters: + user_username (``str``) : username of user + + token (``dict``) : Authorization token + + Returns: `~str`: success + """ + await send_request(f"user/{user_username}/reset", token, "post") + return "success" + + async def revoke_sub(self, user_username: str, token: dict): + """Revoke users subscription (Subscription link and proxies) traffic by username. + + Parameters: + user_username (``str``) : username of user + + token (``dict``) : Authorization token + + Returns: `~str`: success + """ + request = await send_request(f"user/{user_username}/revoke_sub", token, "post") + return User(**request) + + async def get_all_users(self, token: dict, username=None, status=None): + """get all users list. + + Parameters: + token (``dict``) : Authorization token + + Returns: + `~list`: list of users + """ + endpoint = "users" + if username: + endpoint += f"?username={username}" + if status: + if "?" in endpoint: + endpoint += f"&status={status}" + else: + endpoint += f"?status={status}" + request = await send_request(endpoint, token, "get") + user_list = [ + User( + username="", + proxies={}, + inbounds={}, + expire=0, + data_limit=0, + data_limit_reset_strategy="", + ) + ] + for user in request["users"]: + user_list.append(User(**user)) + del user_list[0] + return user_list + + async def reset_all_users_traffic(self, token: dict): + """reset all users traffic. + + Parameters: + token (``dict``) : Authorization token + + Returns: `~str`: success + """ + await send_request("users/reset", token, "post") + return "success" + + async def get_user_usage(self, user_username: str, token: dict): + """get user usage by username. + + Parameters: + user_username (``str``) : username of user + + token (``dict``) : Authorization token + + Returns: `~dict`: dict of user usage + """ + return await end_request(f"user/{user_username}/usage", token, "get")["usages"] + + async def get_all_users_count(self, token: dict): + """get all users count. + + Parameters: + token (``dict``) : Authorization token + + Returns: `~int`: count of users + """ + return await self.get_all_users(token)["content"]["total"] \ No newline at end of file diff --git a/marzpy/marzban.py b/marzpy/marzban.py new file mode 100644 index 0000000..57fb587 --- /dev/null +++ b/marzpy/marzban.py @@ -0,0 +1,9 @@ +from .api import Methods + + +class Marzban(Methods): + def __init__(self, username: str, password: str, panel_address: str) -> None: + super().__init__(username, password, panel_address) + self.username = username + self.password = password + self.panel_address = panel_address diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..36d3dd9 --- /dev/null +++ b/setup.py @@ -0,0 +1,17 @@ +from setuptools import setup, find_packages + +with open("README.md", "r") as file: + readme = file.read() + +setup( + name="marzpy", + version="0.0.4", + author="Mewhrzad", + description="a simple application with python to manage Marzban panel", + long_description="text/markdown", + url="https://github.com/Mewhrzad/marzpy", + keywords=["marzpy", "Marzban", "Gozargah", "Marzban python", "Marzban API"], + packages=find_packages(), + ins=["aiohttp"], + classifiers=["Programming Language :: Python :: 3"], +)