├── .env.example ├── .gitignore ├── Dockerfile ├── README.md ├── docker-compose.prod.yml ├── docker-compose.yml ├── inbounds_gen.sh ├── requirements.txt ├── setup.sh └── src ├── __init__.py ├── data ├── __init__.py ├── db │ ├── __init__.py │ └── get.py ├── engine │ ├── __init__.py │ └── xui.py ├── models │ ├── __init__.py │ ├── admin.py │ ├── clientconfig.py │ └── user.py └── repo │ ├── __init__.py │ ├── admin.py │ ├── clientconfig.py │ └── user.py ├── infrastracture ├── __init__.py ├── config │ ├── __init__.py │ └── env.py ├── db │ ├── __init__.py │ └── sqlite.py └── logger │ ├── __init__.py │ └── std.py ├── logic ├── __init__.py ├── admin.py ├── client.py ├── models │ ├── __init__.py │ ├── client.py │ └── user.py └── user.py ├── main.py └── presentation ├── __init__.py ├── filters ├── __init__.py └── chat_type.py ├── handlers.py ├── kb.py ├── states.py └── text.py /.env.example: -------------------------------------------------------------------------------- 1 | USERNAME= 2 | PASSWORD= 3 | CONFIG_PORT= 4 | EMAIL= 5 | ADMIN_ID= 6 | TELEGRAM_API_TOKEN= -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # poetry 98 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 99 | # This is especially recommended for binary packages to ensure reproducibility, and is more 100 | # commonly ignored for libraries. 101 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 102 | #poetry.lock 103 | 104 | # pdm 105 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 106 | #pdm.lock 107 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 108 | # in version control. 109 | # https://pdm.fming.dev/#use-with-ide 110 | .pdm.toml 111 | 112 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 113 | __pypackages__/ 114 | 115 | # Celery stuff 116 | celerybeat-schedule 117 | celerybeat.pid 118 | 119 | # SageMath parsed files 120 | *.sage.py 121 | 122 | # Environments 123 | .env 124 | .venv 125 | env/ 126 | venv/ 127 | ENV/ 128 | env.bak/ 129 | venv.bak/ 130 | 131 | # Spyder project settings 132 | .spyderproject 133 | .spyproject 134 | 135 | # Rope project settings 136 | .ropeproject 137 | 138 | # mkdocs documentation 139 | /site 140 | 141 | # mypy 142 | .mypy_cache/ 143 | .dmypy.json 144 | dmypy.json 145 | 146 | # Pyre type checker 147 | .pyre/ 148 | 149 | # pytype static type analyzer 150 | .pytype/ 151 | 152 | # Cython debug symbols 153 | cython_debug/ 154 | 155 | # PyCharm 156 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 157 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 158 | # and can be added to the global gitignore or merged into this file. For a more nuclear 159 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 160 | .idea/ 161 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.11-slim-buster as builder 2 | 3 | WORKDIR /app 4 | 5 | ENV PYTHONDONTWRITEBYTECODE 1 6 | ENV PYTHONUNBUFFERED 1 7 | 8 | RUN apt-get update && pip install --upgrade pip 9 | 10 | COPY requirements.txt . 11 | RUN pip wheel --no-cache-dir --no-deps --wheel-dir /app/wheels -r requirements.txt 12 | 13 | FROM python:3.11-slim-buster 14 | 15 | WORKDIR /app 16 | 17 | COPY --from=builder /app/wheels /wheels 18 | COPY --from=builder /app/requirements.txt . 19 | 20 | RUN pip install --no-cache /wheels/* 21 | 22 | COPY . . 23 | 24 | ENTRYPOINT ["python3", "-m", "src.main"] -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Установка контейнера 3X-UI + Traefik + Telegram Bot 2 | Инструкция будет включать в себя: 3 | 4 | 1. Установка контейнера 3X-UI + Traefik + Telegram Bot 5 | 2. Настройка 6 | 7 | Для установки понадобится: 8 | 9 | 1. **VPS или иной сервер с доступом к консоли SSH** 10 | 2. **1 ГБ ОЗУ** 11 | 3. **Debian не ниже версии 9 или Ubuntu не ниже 20.04 (инструкция может работать и на других дистрибутивах, но некоторые детали будут отличатся)** 12 | 13 | Официальный репозиторий 3X-UI: [https://github.com/MHSanaei/3x-ui](https://github.com/MHSanaei/3x-ui) 14 | 15 | Официальный репозиторий форка X-UI: [https://github.com/alireza0/x-ui](https://github.com/alireza0/x-ui) 16 | 17 | Скачиваем скрипт, делаем файл исполняемым и запускаем установку: 18 | 19 | curl -sSL https://raw.githubusercontent.com/torikki-tou/team418/main/setup.sh -o setup.sh && chmod +x setup.sh && ./setup.sh 20 | 21 | Скрипт запросит у вас: 22 | 23 | Имя пользователя для панели администратора - "Enter username for 3X-UI Panel" 24 | 25 | Пароль администратора - "Enter password" 26 | 27 | Порт подключения к панели администратора - "Enter port on which 3X-UI would be available: " 28 | 29 | имя хоста (домен) или IP если домен отсутствует "Enter your hostname:" 30 | 31 | Ваш адрес почты для сертификата LetsEncrypt "Enter your e-mail for certificate:" 32 | 33 | API токен вашего телеграм бота (подробнее [тут](https://medium.com/geekculture/generate-telegram-token-for-bot-api-d26faf9bf064)) "Enter your Telegram bot API token:" 34 | 35 | Имя пользователя Telegram администратора - "Enter your Telegram admin profile" 36 | 37 | Также сервер спросит хотите ли вы заменить таблицу inbounds (она содержит данные о клиентах и конфигурациях прокси, по умолчанию пустая) - первый запуск скрипта заменяет пустую таблицу, последующий запуск заменит данные на дефолтную конфигурацию с Vless XTLS-Reality, порт 443. 38 | 39 | По завершении работы скрипт выдаст адрес подключения к панели администратора. 40 | 41 | 3X-UI, Traefik и Telegram бот установлены и работают. 42 | 43 | Для настройки через Веб UI: 44 | 45 | Для 3X-UI переходим по адресу _https://yourIPorDomain:PORT,_ где yourIPorDomain - IP-адрес вашего сервера или доменное имя, если оно у вас есть и настроено 46 | 47 | Для настройки через Telegram Бота: 48 | От администратора: 49 | 1. Перейдите в вашего бота Telegram 50 | 2. Запустите команду /start 51 | ![](https://telegra.ph/file/77b2a279581c21fd8b5db.png) 52 | 3. Для добавления пользователя - Добавить юзера 53 | 4. Ввести имя пользователя (без @) 54 | 5. ввести количество устройств с которых пользователь может одновременно подключаться 55 | 56 | Для настройки клиентов через админскую панель: 57 | 58 | ![](https://telegra.ph/file/7eb8f8013da91cfbfebe0.png) 59 | Выбираем Меню 60 | ![](https://telegra.ph/file/d085c978b3c622d54a875.png) 61 | Добавить пользователя 62 | ![](https://telegra.ph/file/d2721d1ed8a72f8398b45.png) 63 | Меняем необходимые данные или оставляем по умолчанию (ID должен соответствовать формату UUID) 64 | ![](https://telegra.ph/file/12f1372bb3b3239746968.png) 65 | Ограничение по IP - количество одновременно подключенных устройств по данному пользователю 66 | Flow - xtls-rprx-vision 67 | Общий расход - ограничение расхода (при превышении необходимо будет сбросить счетчик трафика) 68 | Срок действия конфигурации (Дата окончания) - дата истечения конфигурации (будет деактивирована, но не удалена) 69 | ![](https://telegra.ph/file/e97259146bedf9ce7394c.png) 70 | по значку QR - отобразить QR Код для подключения, который можно отсканировать камерой в мобильных клиентах ([v2rayNG](https://github.com/2dust/v2rayNG/releases) или [Nekobox](https://github.com/MatsuriDayo/NekoBoxForAndroid/releases) на Android, [Wings X](https://apps.apple.com/us/app/wings-x/id6446119727)/[FoXray](https://apps.apple.com/us/app/foxray/id6448898396) или [Shadowrocket](https://apps.apple.com/us/app/shadowrocket/id932747118) на iOS) 71 | ![](https://telegra.ph/file/9120e5869e7e5dd352357.png) 72 | по значку I (info) - информация о подключении и ссылка на конфиг (vless://) 73 | 74 | Также по кнопке "Меню" можно сбросить счетчики трафика, добавить пользователей (в том числе сгенерировать разом N аккаунтов по шаблону). 75 | -------------------------------------------------------------------------------- /docker-compose.prod.yml: -------------------------------------------------------------------------------- 1 | version: '3.9' 2 | 3 | services: 4 | 3x-ui: 5 | image: ghcr.io/mhsanaei/3x-ui:latest 6 | container_name: 3x-ui 7 | labels: 8 | - "traefik.enable=true" 9 | - "traefik.http.routers.3x-ui.rule=Host(${HOSTNAME})" 10 | - "traefik.http.routers.3x-ui.tls.certresolver=myresolver" 11 | - "traefik.http.services.3x-ui.loadbalancer.server.port=2053" 12 | environment: 13 | XRAY_VMESS_AEAD_FORCED: "false" 14 | USERNAME: ${USERNAME} 15 | PASSWORD: ${PASSWORD} 16 | CONFIG_PORT: ${CONFIG_PORT} 17 | EMAIL: ${EMAIL} 18 | ports: 19 | - "2096:2096" 20 | - "443:443" 21 | tty: true 22 | restart: unless-stopped 23 | 24 | reverse-proxy: 25 | image: traefik:v2.4 26 | container_name: traefik 27 | command: 28 | - "--api.insecure=false" 29 | - "--providers.docker=true" 30 | - "--providers.docker.exposedbydefault=false" 31 | - "--entrypoints.websecure.address=:20657" 32 | - "--certificatesresolvers.myresolver.acme.httpchallenge=true" 33 | - "--certificatesresolvers.myresolver.acme.httpchallenge.entrypoint=web" 34 | - "--certificatesresolvers.myresolver.acme.email=(${EMAIL})" 35 | - "--certificatesresolvers.myresolver.acme.storage=/letsencrypt/acme.json" 36 | ports: 37 | - "80:80" 38 | - "${CONFIG_PORT}:20657" 39 | volumes: 40 | - "/var/run/docker.sock:/var/run/docker.sock:ro" 41 | - "./letsencrypt:/letsencrypt" 42 | depends_on: 43 | - 3x-ui 44 | restart: unless-stopped 45 | 46 | backend: 47 | image: torikki/team_418:latest 48 | environment: 49 | XUI_LOGIN: ${XUI_LOGIN} 50 | XUI_PASS: ${XUI_PASS} 51 | ADMIN_TELEGRAM_ID: ${ADMIN_IDS} 52 | TELEGRAM_API_TOKEN: ${TELEGRAM_API_TOKEN} -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.9" 2 | 3 | services: 4 | 3x-ui: 5 | image: ghcr.io/mhsanaei/3x-ui:latest 6 | container_name: 3x-ui 7 | labels: 8 | - "traefik.enable=true" 9 | - "traefik.http.routers.3x-ui.rule=Host(`${XUI_HOSTNAME}`)" 10 | - "traefik.http.routers.3x-ui.tls.certresolver=myresolver" 11 | - "traefik.http.services.3x-ui.loadbalancer.server.port=2053" 12 | environment: 13 | XRAY_VMESS_AEAD_FORCED: "false" 14 | XUI_USERNAME: ${XUI_USERNAME} 15 | XUI_PASSWORD: ${XUI_PASSWORD} 16 | XUI_PANEL_PORT: ${XUI_PANEL_PORT} 17 | XUI_HOSTNAME: ${XUI_HOSTNAME} 18 | XUI_EMAIL: ${XUI_EMAIL} 19 | TGTOKEN: ${TGTOKEN} 20 | ADMINID: ${ADMINID} 21 | volumes: 22 | - "$PWD/cert/:/root/cert/" 23 | - "$PWD/db/:/etc/x-ui/" 24 | ports: 25 | - "2096:2096" 26 | - "443:443" 27 | tty: true 28 | restart: unless-stopped 29 | 30 | reverse-proxy: 31 | image: traefik:v2.4 32 | container_name: traefik 33 | command: 34 | - "--api.insecure=false" 35 | - "--providers.docker=true" 36 | - "--providers.docker.exposedbydefault=false" 37 | - "--entrypoints.web.address=:80" 38 | - "--entrypoints.websecure.address=:20657" 39 | - "--certificatesresolvers.myresolver.acme.httpchallenge=true" 40 | - "--certificatesresolvers.myresolver.acme.httpchallenge.entrypoint=web" 41 | - "--certificatesresolvers.myresolver.acme.email=${XUI_EMAIL}" 42 | - "--certificatesresolvers.myresolver.acme.storage=/letsencrypt/acme.json" 43 | labels: 44 | # Middleware for HTTP to HTTPS redirection 45 | - "traefik.http.middlewares.redirect-to-https.redirectscheme.scheme=https" 46 | - "traefik.http.middlewares.redirect-to-https.redirectscheme.permanent=true" 47 | - "traefik.http.middlewares.redirect-to-https.redirectscheme.port=${XUI_PANEL_PORT}" 48 | # Apply middleware to the main router for HTTP 49 | - "traefik.http.routers.redirect.rule=Host(`${XUI_HOSTNAME}`)" 50 | - "traefik.http.routers.redirect.entrypoints=web" 51 | - "traefik.http.routers.redirect.middlewares=redirect-to-https" 52 | ports: 53 | - "${XUI_PANEL_PORT}:20657" 54 | volumes: 55 | - "/var/run/docker.sock:/var/run/docker.sock:ro" 56 | - "$PWD/letsencrypt:/letsencrypt" 57 | depends_on: 58 | - 3x-ui 59 | restart: unless-stopped 60 | 61 | backend: 62 | build: 63 | context: . 64 | dockerfile: Dockerfile 65 | environment: 66 | XUI_LOGIN: ${XUI_USERNAME} 67 | XUI_PASS: ${XUI_PASSWORD} 68 | ADMIN_TELEGRAM_ID: ${ADMINID} 69 | TELEGRAM_API_TOKEN: ${TGTOKEN} 70 | XUI_HOSTNAME: ${XUI_HOSTNAME} 71 | XUI_URL: "http://3x-ui:2053" 72 | volumes: 73 | - "$PWD/db/:/db/" 74 | -------------------------------------------------------------------------------- /inbounds_gen.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Generate values 4 | UUID=$(uuidgen) 5 | output=$(docker exec 3x-ui sh -c "/app/bin/xray-linux-amd64 x25519") 6 | 7 | PRIVATE_KEY=$(echo "$output" | awk -F': ' '/Private key/ {print $2}') 8 | PUBLIC_KEY=$(echo "$output" | awk -F': ' '/Public key/ {print $2}') 9 | 10 | echo "Private key: $PRIVATE_KEY" 11 | echo "Public key: $PUBLIC_KEY" 12 | 13 | # Create inbounds.sql 14 | cat > inbounds.sql << EOF 15 | BEGIN TRANSACTION; 16 | DROP TABLE IF EXISTS "inbounds"; 17 | CREATE TABLE IF NOT EXISTS "inbounds" ( 18 | "id" integer, 19 | "user_id" integer, 20 | "up" integer, 21 | "down" integer, 22 | "total" integer, 23 | "remark" text, 24 | "enable" numeric, 25 | "expiry_time" integer, 26 | "listen" text, 27 | "port" integer UNIQUE, 28 | "protocol" text, 29 | "settings" text, 30 | "stream_settings" text, 31 | "tag" text UNIQUE, 32 | "sniffing" text, 33 | PRIMARY KEY("id") 34 | ); 35 | INSERT INTO "inbounds" VALUES (1,1,0,0,0,'',1,0,'',443,'vless','{ 36 | "clients": [ 37 | { 38 | "id": "$UUID", 39 | "flow": "xtls-rprx-vision", 40 | "email": "default418", 41 | "limitIp": 0, 42 | "totalGB": 0, 43 | "expiryTime": 0, 44 | "enable": true, 45 | "tgId": "", 46 | "subId": "" 47 | } 48 | ], 49 | "decryption": "none", 50 | "fallbacks": [] 51 | }','{ 52 | "network": "tcp", 53 | "security": "reality", 54 | "realitySettings": { 55 | "show": false, 56 | "xver": 0, 57 | "dest": "dl.google.com:443", 58 | "serverNames": [ 59 | "dl.google.com" 60 | ], 61 | "privateKey": "$PRIVATE_KEY", 62 | "minClient": "", 63 | "maxClient": "", 64 | "maxTimediff": 0, 65 | "shortIds": [ 66 | "deced1f3" 67 | ], 68 | "settings": { 69 | "publicKey": "$PUBLIC_KEY", 70 | "fingerprint": "chrome", 71 | "serverName": "", 72 | "spiderX": "/" 73 | } 74 | }, 75 | "tcpSettings": { 76 | "acceptProxyProtocol": false, 77 | "header": { 78 | "type": "none" 79 | } 80 | } 81 | }','inbound-443','{ 82 | "enabled": true, 83 | "destOverride": [ 84 | "http", 85 | "tls", 86 | "quic", 87 | "fakedns" 88 | ] 89 | }'); 90 | COMMIT; 91 | EOF 92 | 93 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | aiofiles==23.1.0 2 | aiogram==3.1.1 3 | aiohttp==3.8.5 4 | aiosignal==1.3.1 5 | annotated-types==0.5.0 6 | async-timeout==4.0.3 7 | attrs==23.1.0 8 | certifi==2023.7.22 9 | charset-normalizer==3.3.0 10 | frozenlist==1.4.0 11 | idna==3.4 12 | magic-filter==1.0.11 13 | multidict==6.0.4 14 | pydantic==2.3.0 15 | pydantic_core==2.6.3 16 | pyxui==0.0.9 17 | requests==2.31.0 18 | shortuuid==1.0.11 19 | typing_extensions==4.7.1 20 | urllib3==2.0.5 21 | yarl==1.9.2 22 | -------------------------------------------------------------------------------- /setup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | #Check if script executed by root 4 | function isRoot() { 5 | if [ "$EUID" -ne 0 ]; then 6 | return 1 7 | fi 8 | } 9 | 10 | #Check if sqlite3 package is installed 11 | function check_sqlite3() { 12 | if ! command -v sqlite3 &> /dev/null; then 13 | apt-get update 14 | apt-get install -y sqlite3 15 | fi 16 | } 17 | 18 | #Check if uuid-runtime package is installed 19 | function check_uuidgen() { 20 | if ! command -v uuidgen &> /dev/null; then 21 | apt-get update 22 | apt-get install -y uuid-runtime 23 | fi 24 | } 25 | #Check if the docker-ce packages are installed 26 | function check_docker() { 27 | packages=(docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin) 28 | for pkg in "${packages[@]}"; do 29 | if ! dpkg-query -l | grep -qw "$pkg"; then 30 | return 1 31 | fi 32 | done 33 | return 0 34 | } 35 | 36 | #Install docker packages from docker.com repo 37 | function install_docker() { 38 | apt-get update 39 | apt-get install -y ca-certificates curl gnupg 40 | install -m 0755 -d /etc/apt/keyrings 41 | if [[ $(lsb_release -is) == "Debian" ]]; then 42 | curl -fsSL https://download.docker.com/linux/debian/gpg | gpg --dearmor -o /etc/apt/keyrings/docker.gpg 43 | echo \ 44 | "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/debian \ 45 | $(lsb_release -cs) stable" | \ 46 | tee /etc/apt/sources.list.d/docker.list > /dev/null 47 | elif [[ $(lsb_release -is) == "Ubuntu" ]]; then 48 | curl -fsSL https://download.docker.com/linux/ubuntu/gpg | gpg --dearmor -o /etc/apt/keyrings/docker.gpg 49 | echo \ 50 | "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \ 51 | $(lsb_release -cs) stable" | \ 52 | tee /etc/apt/sources.list.d/docker.list > /dev/null 53 | else 54 | echo "Unsupported OS" 55 | exit 1 56 | fi 57 | chmod a+r /etc/apt/keyrings/docker.gpg 58 | apt-get update 59 | apt-get install -y "${packages[@]}" 60 | echo -e "\e[34mDocker packages were installed\e[0m" 61 | } 62 | 63 | #Check for unzip package installed 64 | function check_unzip() { 65 | if ! command -v unzip &> /dev/null; then 66 | apt-get update 67 | apt-get install -y unzip 68 | fi 69 | } 70 | 71 | #Checks if "inbounds" table exists in x-ui.db, and if the inbound_gen script can be executed 72 | function check_inbounds_table() { 73 | current_dir=$(pwd) 74 | db_path="$current_dir/db/x-ui.db" 75 | if [[ -f "$db_path" ]]; then 76 | table_exists=$(sqlite3 "$db_path" "SELECT name FROM sqlite_master WHERE type='table' AND name='inbounds';") 77 | if [[ $table_exists == "inbounds" ]]; then 78 | read -p "The 'inbounds' table already exists. On first run it's empty, you can safely continue. Do you want to overwrite existing 3X-UI config? (y/n): " response 79 | if [[ $response == "y" ]]; then 80 | ./inbounds_gen.sh 81 | sqlite3 $db_path < inbounds.sql 82 | echo -e "\e[34mAdded XTLS-Reality config entry into x-ui.db database\e[0m" 83 | else 84 | echo "Skipping execution of inbounds_gen.sh." 85 | fi 86 | else 87 | ./inbounds_gen.sh 88 | sqlite3 $db_path < inbounds.sql 89 | fi 90 | else 91 | echo "x-ui.db does not exist in $db_path. Proceeding with the rest of the setup." 92 | fi 93 | } 94 | 95 | #Check for existing team_418 folder and clones repo (testing) with wget 96 | function clone_repo() { 97 | if [[ -d "team_418" ]]; then 98 | cd team_418 || exit 99 | # Here you might want to fetch and unzip again or just rely on the existing content. 100 | # We're assuming that you want to fetch the newest content. 101 | # So we'll remove the old files, fetch the new .zip and then unzip. 102 | rm -rf * 103 | wget https://github.com/torikki-tou/team418/archive/refs/heads/main.zip 104 | unzip main.zip 105 | mv team418-main/* . 106 | rm -rf team418-main main.zip 107 | echo -e "\e[34m team418 repository has been cloned\e[0m" 108 | else 109 | wget https://github.com/torikki-tou/team418/archive/refs/heads/main.zip 110 | unzip main.zip 111 | mkdir -p team_418 112 | mv team418-main/* team_418/ 113 | cd team_418 || exit 114 | rm -rf ../team418-main ../testing.zip 115 | echo -e "\e[34m team418 repository has been cloned\e[0m" 116 | fi 117 | } 118 | 119 | #Check if user is root, halt if not 120 | if ! isRoot; then 121 | echo "This script must be run as root" 122 | exit 1 123 | fi 124 | 125 | #Docker installation with checking for uuid and sqlite packages 126 | echo -e "Checking for uuidgen package installed...." 127 | check_uuidgen 128 | echo -e "Checking for sqlite3 package installed...." 129 | check_sqlite3 130 | echo -e "Checking for Docker packages installed...." 131 | check_docker 132 | if [[ $? -ne 0 ]]; then 133 | read -p "Docker components are missing, would you like to install them? (y/n) (If not, you would have to install them manually) : " response 134 | if [[ $response == "y" ]]; then 135 | install_docker 136 | echo -e "\e[34mDocker packages were installed\e[0m" 137 | else 138 | echo "Aborting" 139 | exit 1 140 | fi 141 | fi 142 | 143 | #Checks for unzip package 144 | echo -e "Checking for unzip package installed...." 145 | check_unzip 146 | #Clones team418 repo 147 | echo -e "Cloning team_418 repository from Github..." 148 | clone_repo 149 | chmod +x inbounds_gen.sh 150 | 151 | # Function to check existence of a variable in .env 152 | function check_variable() { 153 | local var_name="$1" 154 | if ! grep -q "^$var_name=" .env; then 155 | return 1 156 | fi 157 | return 0 158 | } 159 | 160 | # Function to check for all the variables in .env 161 | function check_all_variables() { 162 | local variables=("XUI_USERNAME" "XUI_PASSWORD" "XUI_PANEL_PORT" "XUI_HOSTNAME" "XUI_EMAIL" "TGTOKEN" "ADMINID") 163 | for var in "${variables[@]}"; do 164 | if ! check_variable "$var"; then 165 | return 1 166 | fi 167 | done 168 | return 0 169 | } 170 | 171 | # Check if all variables exist in .env 172 | if check_all_variables; then 173 | read -p "Variables already exist in .env. Do you want to reinstall admin panel from scratch? (y/n): " response 174 | if [[ $response != "y" ]]; then 175 | echo "Aborting." 176 | exit 0 177 | fi 178 | fi 179 | 180 | echo -e " 181 | _ _ _ ___ 182 | | || | / |( _ ) 183 | | || |_| |/ _ \ 184 | |__ _| | (_) | 185 | |_| |_|\___/ 186 | " 187 | echo -e "\e[32mWelcome to 3X-UI Docker + Traefik + TelegramBot installation script\e[0m" 188 | 189 | read -p "Enter username for 3X-UI Panel: " usernameTemp 190 | read -p "Enter password (only numbers and letters, no special characters): " passwordTemp 191 | read -p "Enter port on which 3X-UI web admin panel would be available: " config_port 192 | read -p "Enter your hostname (IP or Domain):" hostname_input 193 | read -p "Enter your e-mail for certificate :" email_input 194 | read -p "Enter your Telegram bot API token (use Tg BotFather):" tgtoken_input 195 | read -p "Enter your Telegram admin profile (as @admin without @):" tgadminid_input 196 | 197 | #Export variables to docker-compose 198 | export XUI_USERNAME=$usernameTemp 199 | export XUI_PASSWORD=$passwordTemp 200 | export XUI_PANEL_PORT=$config_port 201 | export XUI_HOSTNAME=$hostname_input 202 | export XUI_EMAIL=$email_input 203 | export TGTOKEN=$tgtoken_input 204 | export ADMINID=$tgadminid_input 205 | 206 | #Export variables to .env file 207 | echo "XUI_USERNAME=$XUI_USERNAME" > .env 208 | echo "XUI_PASSWORD=$XUI_PASSWORD" >> .env 209 | echo "XUI_PANEL_PORT=$XUI_PANEL_PORT" >> .env 210 | echo "XUI_HOSTNAME=$XUI_HOSTNAME" >> .env 211 | echo "XUI_EMAIL=$XUI_EMAIL" >> .env 212 | echo "TGTOKEN=$TGTOKEN" >> .env 213 | echo "ADMINID=$ADMINID" >> .env 214 | 215 | docker compose up -d 216 | docker exec 3x-ui sh -c "/app/x-ui setting -username $usernameTemp -password $passwordTemp" 217 | echo -e "username and password applied to 3X-UI Container" 218 | sleep 1 219 | docker restart 3x-ui 220 | echo -e "3X-UI Docker container restarted" 221 | sleep 3 222 | #Adds default config for XTLS-Reality into x-ui.db if conditions met (prompt y/n) 223 | check_inbounds_table 224 | 225 | docker restart 3x-ui 226 | echo -e "\e[32m3X-UI + Traefik + TelegramBot installation finished, XTLS-Reality default config added, admin panel is available on https://$hostname_input:$config_port\e[0m" 227 | 228 | -------------------------------------------------------------------------------- /src/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/torikki-tou/team418/bc0717113e1b903c846cb6c731405b3ebd630662/src/__init__.py -------------------------------------------------------------------------------- /src/data/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/torikki-tou/team418/bc0717113e1b903c846cb6c731405b3ebd630662/src/data/__init__.py -------------------------------------------------------------------------------- /src/data/db/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/torikki-tou/team418/bc0717113e1b903c846cb6c731405b3ebd630662/src/data/db/__init__.py -------------------------------------------------------------------------------- /src/data/db/get.py: -------------------------------------------------------------------------------- 1 | import sqlite3 2 | 3 | db_path = 'db418.db' 4 | 5 | 6 | def get_user_db() -> sqlite3.Connection: 7 | conn = sqlite3.connect(db_path) 8 | 9 | conn.execute( 10 | ''' 11 | CREATE TABLE IF NOT EXISTS users 12 | (ID TEXT PRIMARY KEY NOT NULL, 13 | CFG_LIMIT INT NOT NULL); 14 | ''' 15 | ) 16 | 17 | return conn 18 | 19 | 20 | def get_client_db() -> sqlite3.Connection: 21 | conn = sqlite3.connect(db_path) 22 | 23 | conn.execute( 24 | ''' 25 | CREATE TABLE IF NOT EXISTS clients 26 | (ID TEXT PRIMARY KEY NOT NULL, 27 | USER_ID TEXT NOT NULL); 28 | ''' 29 | ) 30 | 31 | return conn 32 | -------------------------------------------------------------------------------- /src/data/engine/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/torikki-tou/team418/bc0717113e1b903c846cb6c731405b3ebd630662/src/data/engine/__init__.py -------------------------------------------------------------------------------- /src/data/engine/xui.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | 3 | from pyxui import XUI 4 | from pyxui.errors import BadLogin, NotFound 5 | import uuid 6 | 7 | from src.infrastracture.config import config 8 | from src.infrastracture.logger import logger 9 | 10 | 11 | class XUIClient: 12 | def __init__(self): 13 | self.__client: XUI = (XUI( 14 | full_address=config.get_engine_url(), 15 | panel='sanaei' 16 | )) 17 | if not self.__login(): 18 | logger.critical('xui login unsuccessful') 19 | 20 | self.__default_inbound_id = 1 21 | return 22 | 23 | def get_server_info(self) -> dict: 24 | return self.__client.get_inbound(self.__default_inbound_id)['obj'] 25 | 26 | def get_client(self, client_id: str) -> Optional[dict]: 27 | res = self.__client.get_client(self.__default_inbound_id, client_id) 28 | 29 | if res is NotFound: 30 | return None 31 | 32 | return res 33 | 34 | def create_client(self, prefix: str) -> str: 35 | client_uuid = str(uuid.uuid4()) 36 | 37 | client_id = prefix + '_' + client_uuid if prefix else client_uuid 38 | 39 | self.__client.add_client( 40 | self.__default_inbound_id, 41 | client_id, 42 | client_uuid 43 | ) 44 | 45 | return client_uuid 46 | 47 | def delete_client(self, client_id: str) -> bool: 48 | try: 49 | self.__client.delete_client( 50 | self.__default_inbound_id, 51 | client_id 52 | ) 53 | except Exception: 54 | return False 55 | 56 | return True 57 | 58 | def __login(self) -> bool: 59 | try: 60 | self.__client.login( 61 | username=config.get_engine_username(), 62 | password=config.get_engine_password(), 63 | ) 64 | except BadLogin: 65 | return False 66 | 67 | return True 68 | 69 | 70 | xui_engine = XUIClient() 71 | -------------------------------------------------------------------------------- /src/data/models/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/torikki-tou/team418/bc0717113e1b903c846cb6c731405b3ebd630662/src/data/models/__init__.py -------------------------------------------------------------------------------- /src/data/models/admin.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel 2 | 3 | 4 | class Admin(BaseModel): 5 | id: str 6 | -------------------------------------------------------------------------------- /src/data/models/clientconfig.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | 3 | from pydantic import BaseModel 4 | 5 | 6 | class ClientConfig(BaseModel): 7 | id: str 8 | user_id: Optional[str] 9 | comment: Optional[str] 10 | -------------------------------------------------------------------------------- /src/data/models/user.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel 2 | 3 | 4 | class User(BaseModel): 5 | id: str 6 | limit: int 7 | -------------------------------------------------------------------------------- /src/data/repo/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/torikki-tou/team418/bc0717113e1b903c846cb6c731405b3ebd630662/src/data/repo/__init__.py -------------------------------------------------------------------------------- /src/data/repo/admin.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | from src.data.models.admin import Admin as AdminDTO 4 | 5 | from src.infrastracture.config import config 6 | from src.infrastracture.logger import logger 7 | 8 | 9 | class Admin: 10 | def __init__(self): 11 | admins = config.get_admins_ids() 12 | if not admins: 13 | logger.critical('no admin ids found') 14 | self.__admins: List[str] = admins 15 | return 16 | 17 | def get_all(self) -> List[AdminDTO]: 18 | return [AdminDTO(**{'id': admin_id}) for admin_id in self.__admins] 19 | -------------------------------------------------------------------------------- /src/data/repo/clientconfig.py: -------------------------------------------------------------------------------- 1 | from typing import Optional, List 2 | 3 | from src.data.models.clientconfig import ClientConfig as ClientDTO 4 | from src.infrastracture.db import db 5 | 6 | 7 | class ClientConfig: 8 | def __init__(self): 9 | self.__con = db.get_connection() 10 | self.__create_clients_table() 11 | return 12 | 13 | def get_by_user(self, user_id: Optional[str], offset: int = 0, limit: int = 20) -> List[ClientDTO]: 14 | query = ''' 15 | SELECT * FROM clients WHERE user_id = ? OFFSET ? LIMIT ? 16 | ''' 17 | 18 | res = self.__con.cursor().execute( 19 | query, 20 | (user_id, offset, limit) 21 | ).fetchall() 22 | 23 | return [ClientDTO(**{'id': e[0], 'user_id': e[1]}) for e in res] 24 | 25 | def count_by_user(self, user_id: Optional[str]) -> int: 26 | query = ''' 27 | SELECT count(*) FROM clients WHERE user_id = ? 28 | ''' 29 | 30 | res = self.__con.cursor().execute( 31 | query, 32 | (user_id,) 33 | ).fetchone() 34 | 35 | return res[0] 36 | 37 | def get(self, client_id: str) -> Optional[ClientDTO]: 38 | query = ''' 39 | SELECT * FROM clients WHERE id = ? 40 | ''' 41 | 42 | res = self.__con.cursor().execute( 43 | query, (client_id,) 44 | ).fetchone() 45 | 46 | if res is None: 47 | return None 48 | 49 | return ClientDTO(**{'id': res[0], 'user_id': res[1]}) 50 | 51 | def create(self, client_id: str, user_id: Optional[str], comment: Optional[str]) -> None: 52 | query = ''' 53 | INSERT OR REPLACE INTO clients VALUES (?, ?, ?) 54 | ''' 55 | 56 | self.__con.cursor().execute(query, ( 57 | client_id, user_id, comment 58 | )) 59 | 60 | self.__con.commit() 61 | return 62 | 63 | def delete(self, client_id: str) -> None: 64 | query = ''' 65 | DELETE FROM clients WHERE id = ? 66 | ''' 67 | 68 | self.__con.cursor().execute(query, (client_id,)) 69 | 70 | self.__con.commit() 71 | return 72 | 73 | def __create_clients_table(self): 74 | self.__con.execute( 75 | ''' 76 | CREATE TABLE IF NOT EXISTS clients ( 77 | ID TEXT PRIMARY KEY NOT NULL, 78 | USER_ID TEXT 79 | COMMENT TEXT 80 | ); 81 | ''' 82 | ) 83 | 84 | self.__con.commit() 85 | -------------------------------------------------------------------------------- /src/data/repo/user.py: -------------------------------------------------------------------------------- 1 | from typing import Optional, List 2 | 3 | from src.data.models.user import User as UserDTO 4 | from src.infrastracture.db import db 5 | 6 | 7 | class User: 8 | def __init__(self): 9 | self.__con = db.get_connection() 10 | self.__create_user_table() 11 | return 12 | 13 | def get_many(self, offset: int = 0, limit: int = 20) -> List[UserDTO]: 14 | query = ''' 15 | SELECT * FROM users OFFSET ? LIMIT ? 16 | ''' 17 | 18 | res = self.__con.cursor().execute( 19 | query, 20 | (offset, limit) 21 | ).fetchall() 22 | 23 | return [UserDTO(**{'id': e[0], 'limit': e[1]}) for e in res] 24 | 25 | def get(self, user_id: str) -> Optional[UserDTO]: 26 | query = ''' 27 | SELECT * FROM users WHERE id = ? 28 | ''' 29 | 30 | res = self.__con.cursor().execute(query, (user_id,)).fetchone() 31 | 32 | if res is None: 33 | return None 34 | 35 | return UserDTO(**{'id': res[0], 'limit': res[1]}) 36 | 37 | def create(self, user_id: str, limit: int) -> None: 38 | query = ''' 39 | INSERT OR REPLACE INTO users VALUES (?, ?) 40 | ''' 41 | 42 | self.__con.cursor().execute(query, (user_id, limit)) 43 | 44 | self.__con.commit() 45 | return 46 | 47 | def delete(self, user_id: str) -> bool: 48 | query = ''' 49 | DELETE FROM users WHERE id = ? 50 | ''' 51 | 52 | self.__con.cursor().execute(query, (user_id,)) 53 | 54 | self.__con.commit() 55 | return True 56 | 57 | def __create_user_table(self): 58 | self.__con.execute( 59 | ''' 60 | CREATE TABLE IF NOT EXISTS users ( 61 | ID TEXT PRIMARY KEY NOT NULL, 62 | CFG_LIMIT INT NOT NULL 63 | ); 64 | ''' 65 | ) 66 | 67 | self.__con.commit() 68 | -------------------------------------------------------------------------------- /src/infrastracture/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/torikki-tou/team418/bc0717113e1b903c846cb6c731405b3ebd630662/src/infrastracture/__init__.py -------------------------------------------------------------------------------- /src/infrastracture/config/__init__.py: -------------------------------------------------------------------------------- 1 | from src.infrastracture.config.env import env_config as config 2 | -------------------------------------------------------------------------------- /src/infrastracture/config/env.py: -------------------------------------------------------------------------------- 1 | import os 2 | from typing import List 3 | 4 | 5 | class EnvConfig: 6 | def __init__(self): 7 | self.__read_telegram_api_token() 8 | self.__read_admins_ids() 9 | self.__read_chat_ids() 10 | self.__read_default_max_configs() 11 | self.__read_engine_url() 12 | self.__read_engine_username() 13 | self.__read_server_hostname() 14 | self.__read_engine_password() 15 | 16 | def get_telegram_api_token(self, refresh: bool = False) -> str: 17 | if refresh: 18 | self.__read_telegram_api_token() 19 | 20 | return self.__telegram_api_token 21 | 22 | def get_admins_ids(self, refresh: bool = False) -> List[str]: 23 | if refresh: 24 | self.__read_admins_ids() 25 | 26 | return self.__admins_ids 27 | 28 | def get_chat_ids(self, refresh: bool = False) -> List[str]: 29 | if refresh: 30 | self.__read_chat_ids() 31 | 32 | return self.__chat_ids 33 | 34 | def get_default_max_configs(self, refresh: bool = False) -> int: 35 | if refresh: 36 | self.__read_default_max_configs() 37 | 38 | return self.__default_max_configs 39 | 40 | def get_engine_url(self, refresh: bool = False) -> str: 41 | if refresh: 42 | self.__read_engine_url() 43 | 44 | return self.__engine_url 45 | 46 | def get_server_hostname(self, refresh: bool = False) -> str: 47 | if refresh: 48 | self.__read_server_hostname() 49 | 50 | return self.__server_hostname 51 | 52 | def get_engine_username(self, refresh: bool = False) -> str: 53 | if refresh: 54 | self.__read_engine_username() 55 | 56 | return self.__engine_username 57 | 58 | def get_engine_password(self, refresh: bool = False) -> str: 59 | if refresh: 60 | self.__read_engine_password() 61 | 62 | return self.__engine_password 63 | 64 | def get_allow_chat_members(self, refresh: bool = False) -> bool: 65 | if refresh: 66 | self.__read_allow_chat_members() 67 | 68 | return self.__allow_chat_members 69 | 70 | def __read_telegram_api_token(self): 71 | self.__telegram_api_token: str = (os.environ.get('TELEGRAM_API_TOKEN') 72 | or None) 73 | 74 | def __read_admins_ids(self): 75 | admins_ids: str = os.environ.get('') or '' 76 | self.__admins_ids: List[str] = [admin_id.strip('@') for admin_id in 77 | (admins_ids.split(',') or [])] 78 | 79 | def __read_chat_ids(self): 80 | chat_ids: str = os.environ.get('') or '' 81 | self.__chat_ids: List[str] = chat_ids.split(',') or [] 82 | 83 | def __read_default_max_configs(self): 84 | self.__default_max_configs: int = os.environ.get('') or 3 85 | 86 | def __read_engine_url(self): 87 | self.__engine_url: str = (os.environ.get('XUI_URL') 88 | or 'http://3x-ui:2053') 89 | 90 | def __read_server_hostname(self): 91 | self.__server_hostname: str = (os.environ.get('XUI_HOSTNAME') 92 | or '0.0.0.0') 93 | 94 | def __read_engine_username(self): 95 | self.__engine_username: str = os.environ.get('XUI_LOGIN') or 'admin' 96 | 97 | def __read_engine_password(self): 98 | self.__engine_password: str = os.environ.get('XUI_PASS') or 'admin' 99 | 100 | def __read_allow_chat_members(self): 101 | self.__allow_chat_members: bool = os.environ.get('ALLOW_CHAT_MEMBERS') or False 102 | 103 | 104 | env_config = EnvConfig() 105 | -------------------------------------------------------------------------------- /src/infrastracture/db/__init__.py: -------------------------------------------------------------------------------- 1 | from src.infrastracture.db.sqlite import sqlite_con as db 2 | -------------------------------------------------------------------------------- /src/infrastracture/db/sqlite.py: -------------------------------------------------------------------------------- 1 | import sqlite3 2 | from typing import Generator 3 | 4 | 5 | class SQLiteCon: 6 | def __init__(self): 7 | self.__connection = sqlite3.connect('/db/db418.db') 8 | 9 | def get_connection(self) -> sqlite3.Connection: 10 | return self.__connection 11 | 12 | 13 | sqlite_con = SQLiteCon() 14 | -------------------------------------------------------------------------------- /src/infrastracture/logger/__init__.py: -------------------------------------------------------------------------------- 1 | from src.infrastracture.logger.std import std_logger as logger 2 | -------------------------------------------------------------------------------- /src/infrastracture/logger/std.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | 4 | std_logger = logging.getLogger() 5 | -------------------------------------------------------------------------------- /src/logic/__init__.py: -------------------------------------------------------------------------------- 1 | from src.logic.user import User 2 | from src.logic.client import Client 3 | from src.logic.admin import Admin 4 | -------------------------------------------------------------------------------- /src/logic/admin.py: -------------------------------------------------------------------------------- 1 | from src.data.repo.admin import Admin as AdminRepo 2 | 3 | 4 | class Admin: 5 | def __init__(self): 6 | self.data = AdminRepo() 7 | return 8 | 9 | def is_admin(self, telegram_id: str) -> bool: 10 | for admin in self.data.get_all(): 11 | if admin.id == telegram_id: 12 | return True 13 | 14 | return False 15 | -------------------------------------------------------------------------------- /src/logic/client.py: -------------------------------------------------------------------------------- 1 | from typing import Optional, List, Dict 2 | import json 3 | 4 | from pyxui.config_gen import config_generator 5 | 6 | from src.logic.models.client import Client as ClientDTO 7 | from src.data.repo.clientconfig import ClientConfig as ClientRepo 8 | from src.data.engine.xui import XUIClient 9 | from src.infrastracture.config import config 10 | 11 | 12 | class Client: 13 | def __init__(self): 14 | self.client_repo = ClientRepo() 15 | self.xui = XUIClient() 16 | return 17 | 18 | def get_by_user(self, user_id: str, offset: int = 0, limit: int = 20) -> List[str]: 19 | return [cnf.id for cnf in self.client_repo.get_by_user(user_id, offset, limit)] 20 | 21 | def get(self, client_id: str) -> Optional[ClientDTO]: 22 | db_client = self.client_repo.get(client_id) 23 | 24 | if db_client is None: 25 | return None 26 | 27 | stream_settings = json.loads( 28 | self.xui.get_server_info()['streamSettings']) 29 | 30 | settings = stream_settings['realitySettings']['settings'] 31 | 32 | client_config = { 33 | "ps": db_client.id, 34 | "add": config.get_server_hostname(), 35 | "port": "443", 36 | "id": db_client.id 37 | } 38 | 39 | client_data = { 40 | "pbk": settings['publicKey'], 41 | "security": "reality", 42 | "type": "tcp", 43 | "sni": "dl.google.com", 44 | "spx": "/", 45 | "sid": "deced1f3", 46 | "fp": "firefox" 47 | } 48 | 49 | conn_str = config_generator("vless", client_config, client_data) 50 | 51 | return ClientDTO(**{'id': db_client.id, 'conn_str': conn_str}) 52 | 53 | def create(self, user_id: Optional[str] = None, comment: Optional[str] = None) -> str: 54 | client_id = self.xui.create_client(user_id) 55 | self.client_repo.create(client_id, user_id, comment) 56 | return client_id 57 | 58 | def delete(self, client_id: str) -> bool: 59 | self.client_repo.delete(client_id) 60 | self.xui.delete_client(client_id) 61 | return True 62 | -------------------------------------------------------------------------------- /src/logic/models/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/torikki-tou/team418/bc0717113e1b903c846cb6c731405b3ebd630662/src/logic/models/__init__.py -------------------------------------------------------------------------------- /src/logic/models/client.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel 2 | 3 | 4 | class Client(BaseModel): 5 | id: str 6 | conn_str: str 7 | -------------------------------------------------------------------------------- /src/logic/models/user.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel 2 | 3 | 4 | class User(BaseModel): 5 | id: str 6 | limit: int 7 | -------------------------------------------------------------------------------- /src/logic/user.py: -------------------------------------------------------------------------------- 1 | from typing import Optional, List, Dict 2 | 3 | from src.data.repo.user import User as UserRepo 4 | from src.data.repo.clientconfig import ClientConfig as ClientRepo 5 | from src.logic.models.user import User as UserDTO 6 | 7 | 8 | class User: 9 | def __init__(self): 10 | self.user_repo = UserRepo() 11 | self.client_repo = ClientRepo() 12 | return 13 | 14 | def get_many(self, offset: int = 0, limit: int = 20) -> List[UserDTO]: 15 | return self.user_repo.get_many(offset, limit) 16 | 17 | def allowed_to_create_client(self, user_id: str) -> bool: 18 | user = self.user_repo.get(user_id) 19 | if user is None: 20 | return False 21 | 22 | return self.client_repo.count_by_user(user_id) < user.limit 23 | 24 | def get(self, user_id: str) -> Optional[UserDTO]: 25 | return self.user_repo.get(user_id) 26 | 27 | def create(self, user_id: str, limit: int) -> bool: 28 | if self.user_repo.get(user_id) is not None: 29 | return False 30 | 31 | self.user_repo.create(user_id, limit) 32 | return True 33 | 34 | def delete(self, user_id: str) -> bool: 35 | return self.user_repo.delete(user_id) 36 | 37 | -------------------------------------------------------------------------------- /src/main.py: -------------------------------------------------------------------------------- 1 | import os 2 | import logging 3 | import sys 4 | import asyncio 5 | from aiogram import Bot, Dispatcher 6 | from aiogram.enums import ParseMode 7 | from aiogram.fsm.storage.memory import MemoryStorage 8 | 9 | from src.presentation.handlers import router 10 | from src.infrastracture.config import config 11 | 12 | 13 | async def main() -> None: 14 | bot = Bot(token=config.get_telegram_api_token(), parse_mode=ParseMode.HTML) 15 | dp = Dispatcher(storage=MemoryStorage()) 16 | dp.include_router(router) 17 | await dp.start_polling(bot) 18 | 19 | 20 | if __name__ == "__main__": 21 | logging.basicConfig(level=logging.INFO, stream=sys.stdout) 22 | asyncio.run(main()) 23 | -------------------------------------------------------------------------------- /src/presentation/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/torikki-tou/team418/bc0717113e1b903c846cb6c731405b3ebd630662/src/presentation/__init__.py -------------------------------------------------------------------------------- /src/presentation/filters/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/torikki-tou/team418/bc0717113e1b903c846cb6c731405b3ebd630662/src/presentation/filters/__init__.py -------------------------------------------------------------------------------- /src/presentation/filters/chat_type.py: -------------------------------------------------------------------------------- 1 | from typing import Union 2 | 3 | from aiogram.filters import BaseFilter 4 | from aiogram.types import Message 5 | 6 | 7 | class ChatTypeFilter(BaseFilter): # [1] 8 | def __init__(self, chat_type: Union[str, list]): # [2] 9 | self.chat_type = chat_type 10 | 11 | async def __call__(self, message: Message) -> bool: # [3] 12 | if isinstance(self.chat_type, str): 13 | return message.chat.type == self.chat_type 14 | else: 15 | return message.chat.type in self.chat_type 16 | -------------------------------------------------------------------------------- /src/presentation/handlers.py: -------------------------------------------------------------------------------- 1 | from os import getenv 2 | 3 | from aiogram.filters.callback_data import CallbackData 4 | 5 | from src.logic import Admin, Client, User 6 | from aiogram import Router, F 7 | from aiogram.filters import Command 8 | from aiogram.fsm.context import FSMContext 9 | from aiogram.types import Message, CallbackQuery 10 | from aiogram import flags 11 | 12 | import src.presentation.kb as kb 13 | import src.presentation.text as text 14 | from src.presentation.states import Gen, Del, GenConf, DelConf 15 | 16 | router = Router() 17 | admin_id = getenv("ADMIN_TELEGRAM_ID") 18 | 19 | @router.message(Command("start")) 20 | async def start_handler(msg: Message, state: FSMContext): 21 | await state.clear() 22 | 23 | if msg.from_user.username == admin_id: 24 | await msg.answer(text.hello_admin, reply_markup=kb.admin_menu) 25 | else: 26 | await msg.answer(text.hello_clint, reply_markup=kb.client_menu) 27 | 28 | 29 | @router.callback_query(F.data == "main_menu") 30 | async def menu_handler(clbck: CallbackQuery, state: FSMContext): 31 | await state.clear() 32 | if clbck.from_user.username == admin_id: 33 | await clbck.message.answer(text=text.main_menu, reply_markup=kb.admin_menu) 34 | else: 35 | await clbck.message.answer(text=text.main_menu, reply_markup=kb.client_menu) 36 | 37 | 38 | @router.callback_query(F.data == "get_instructions") 39 | async def get_instructions(clbck: CallbackQuery): 40 | await clbck.message.answer(text=text.instructions, reply_markup=kb.instruction_menu) 41 | 42 | 43 | @router.callback_query(F.data == "add_client") 44 | async def add_client(clbck: CallbackQuery, state: FSMContext): 45 | if clbck.from_user.username == admin_id: 46 | await clbck.message.answer(text=text.client_id_await, reply_markup=kb.iexit_kb) 47 | await state.set_state(Gen().typing_telegram_id) 48 | 49 | 50 | @router.message(Gen.typing_telegram_id) 51 | async def get_telegram_id(msg: Message, state: FSMContext): 52 | if msg.from_user.username == admin_id: 53 | tg_id = msg.text 54 | await state.update_data(chosen_id=tg_id) 55 | await msg.answer(text.client_limit_await, reply_markup=kb.iexit_kb) 56 | await state.set_state(Gen().typing_limit) 57 | 58 | 59 | @router.message(Gen.typing_limit) 60 | async def get_limit(msg: Message, state: FSMContext): 61 | if msg.from_user.username == admin_id: 62 | limit = msg.text 63 | if not limit.isnumeric(): 64 | await state.clear() 65 | return await msg.answer(text.limit_error, reply_markup=kb.iexit_kb) 66 | else: 67 | limit = int(limit) 68 | user_data = await state.get_data() 69 | tg_id = user_data['chosen_id'] 70 | User().create(user_id=tg_id, limit=limit) 71 | await msg.answer(text.user_is_created, reply_markup=kb.iexit_kb) 72 | await state.clear() 73 | 74 | 75 | @router.callback_query(F.data == "delete_client") 76 | async def add_client(clbck: CallbackQuery, state: FSMContext): 77 | if clbck.from_user.username == admin_id: 78 | await clbck.message.answer(text.client_id_await, reply_markup=kb.iexit_kb) 79 | await state.set_state(Del().typing_telegram_id) 80 | 81 | 82 | @router.message(Gen.typing_telegram_id) 83 | async def delete_telegram_id(msg: Message, state: FSMContext): 84 | if msg.from_user.username == admin_id: 85 | tg_id = msg.text 86 | User().delete(user_id=tg_id) 87 | await state.clear() 88 | 89 | 90 | @router.callback_query(F.data == "create_config") 91 | async def add_config(clbck: CallbackQuery): 92 | user_id = str(clbck.from_user.username) 93 | if User().get(user_id) is None: 94 | return await clbck.message.answer(text.user_not_defined, reply_markup=kb.iexit_kb) 95 | if not User().allowed_to_create_client(user_id): 96 | return await clbck.message.answer(text.user_limit_exited, reply_markup=kb.iexit_kb) 97 | client_id = Client().create(user_id=user_id) 98 | client = Client().get(client_id) 99 | uri = client.conn_str 100 | await clbck.message.answer(text=uri, reply_markup=kb.iexit_kb) 101 | 102 | 103 | @router.callback_query(F.data == "delete_config") 104 | async def delete_config(clbck: CallbackQuery, state: FSMContext): 105 | user_id = str(clbck.from_user.username) 106 | if User().get(user_id) is None: 107 | return await clbck.message.answer(text.user_not_defined, reply_markup=kb.iexit_kb) 108 | if not User().allowed_to_create_client(user_id): 109 | return await clbck.message.answer(text.user_limit_exited, reply_markup=kb.iexit_kb) 110 | await clbck.message.answer(text.config_id_await, reply_markup=kb.iexit_kb) 111 | await state.set_state(DelConf().typing_conf_id) 112 | 113 | 114 | @router.message(DelConf.typing_conf_id) 115 | async def delete_config_id(msg: Message, state: FSMContext): 116 | conf_id = msg.text 117 | if not conf_id.isalnum(): 118 | await state.clear() 119 | return await msg.answer(text.config_id_error, reply_markup=kb.iexit_kb) 120 | else: 121 | client_id = msg.text 122 | if Client().get(client_id) is None: 123 | return await msg.answer(text.client_not_defined, reply_markup=kb.iexit_kb) 124 | Client().delete(client_id=client_id) 125 | await state.clear() 126 | await msg.answer(text.config_is_deleted, reply_markup=kb.iexit_kb) 127 | 128 | 129 | @router.callback_query(F.data == "conf_list") 130 | async def add_config(clbck: CallbackQuery): 131 | user_id = clbck.from_user.username 132 | client_ids = Client().get_by_user(str(user_id)) 133 | if client_ids is None or not client_ids: 134 | return await clbck.message.answer(text=text.configs_not_found) 135 | for client_id in client_ids: 136 | client = Client().get(client_id) 137 | uri = client.conn_str 138 | await clbck.message.answer(text=uri) 139 | 140 | 141 | @router.callback_query(F.data == "instruction_ios") 142 | async def get_ios_instruction(clbck: CallbackQuery): 143 | await clbck.message.answer(text=text.instruction_ios, reply_markup=kb.iexit_kb) 144 | 145 | 146 | @router.callback_query(F.data == "instruction_android") 147 | async def get_android_instruction(clbck: CallbackQuery): 148 | await clbck.message.answer(text=text.instruction_android, reply_markup=kb.iexit_kb) 149 | 150 | 151 | @router.callback_query(F.data == "instruction_macos") 152 | async def get_macos_instruction(clbck: CallbackQuery): 153 | await clbck.message.answer(text=text.instruction_macos, reply_markup=kb.iexit_kb) 154 | 155 | 156 | @router.callback_query(F.data == "instruction_windows") 157 | async def get_windows_instruction(clbck: CallbackQuery): 158 | await clbck.message.answer(text=text.instruction_windows, reply_markup=kb.iexit_kb) 159 | 160 | -------------------------------------------------------------------------------- /src/presentation/kb.py: -------------------------------------------------------------------------------- 1 | from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup, KeyboardButton, ReplyKeyboardMarkup, \ 2 | ReplyKeyboardRemove 3 | from aiogram.utils.keyboard import InlineKeyboardBuilder 4 | 5 | from src.logic import Admin, Client, User 6 | 7 | admin_menu = [ 8 | [InlineKeyboardButton(text="➕ Добавить юзера", callback_data="add_client"), 9 | InlineKeyboardButton(text="❌ Удалить юзера", callback_data="delete_client")] 10 | ] 11 | 12 | client_menu = [ 13 | [InlineKeyboardButton(text="📃 Мои конфигурации", callback_data="conf_list"), 14 | InlineKeyboardButton(text="🔧 Запросить конфигурацию", callback_data="create_config")], 15 | [InlineKeyboardButton(text="❌ Удалить Конфигурацию", callback_data="delete_config"), 16 | InlineKeyboardButton(text="🔍 Инструкции", callback_data="get_instructions")] 17 | ] 18 | instruction_menu = [ 19 | [InlineKeyboardButton(text="iOS", callback_data="instruction_ios"), 20 | InlineKeyboardButton(text="Android", callback_data="instruction_android")], 21 | [InlineKeyboardButton(text="MacOS", callback_data="instruction_macos"), 22 | InlineKeyboardButton(text="Windows", callback_data="instruction_windows")], 23 | [InlineKeyboardButton(text="◀️ Выйти в меню", callback_data="main_menu")] 24 | ] 25 | 26 | admin_menu = InlineKeyboardMarkup(inline_keyboard=admin_menu) 27 | client_menu = InlineKeyboardMarkup(inline_keyboard=client_menu) 28 | instruction_menu = InlineKeyboardMarkup(inline_keyboard=instruction_menu) 29 | 30 | iexit_kb = InlineKeyboardMarkup(inline_keyboard=[[InlineKeyboardButton(text="◀️ Выйти в меню", callback_data="main_menu")]]) 31 | 32 | 33 | # def make_config_list(user_id: str) -> InlineKeyboardMarkup: 34 | # builder = InlineKeyboardBuilder() 35 | # client_ids = Client().get_by_user(user_id) 36 | # for client_id in client_ids: 37 | # builder.button(text=client_id, callback_data=cd_conf.) 38 | # return builder.as_markup() 39 | # 40 | # 41 | # def make_handlers_list(user_id: str) -> list: 42 | # 43 | # callbacks = [] 44 | # for client_id in client_ids: 45 | # callbacks.append(f"btn_{client_id}") 46 | # return callbacks 47 | -------------------------------------------------------------------------------- /src/presentation/states.py: -------------------------------------------------------------------------------- 1 | from aiogram.fsm.state import StatesGroup, State 2 | 3 | 4 | class Gen(StatesGroup): 5 | typing_telegram_id = State() 6 | typing_limit = State() 7 | 8 | 9 | class Del(StatesGroup): 10 | typing_telegram_id = State() 11 | 12 | 13 | class GenConf(StatesGroup): 14 | typing_conf_id = State() 15 | 16 | 17 | class DelConf(StatesGroup): 18 | typing_conf_id = State() 19 | -------------------------------------------------------------------------------- /src/presentation/text.py: -------------------------------------------------------------------------------- 1 | hello_admin = """ 2 | Добрый день! 3 | 4 | Это админская панель управления. 5 | Здесь вы можете добавить и удалить клиентские конфигурации. 6 | """ 7 | 8 | hello_clint = """ 9 | Добрый день! 10 | 11 | Прочитайте инструкцию, чтобы понять, что с этим делать 12 | """ 13 | 14 | instructions = """ 15 | Выберите операционную систему 16 | """ 17 | 18 | client_id_await = """ 19 | Напишите Telegram ID пользователя. 20 | Чтобы отменить действие, нажмите на кнопку ниже 21 | """ 22 | 23 | client_limit_await = """ 24 | Напишите лимит подключений пользователя. 25 | Чтобы отменить действие, нажмите на кнопку ниже 26 | """ 27 | 28 | telegram_id_error = """ 29 | Ошибка! Введен некорректный Telegram ID. 30 | Чтобы вернуться, нажмите на кнопку ниже. 31 | """ 32 | 33 | limit_error = """ 34 | Ошибка! Введен некорректный лимит. 35 | Чтобы вернуться, нажмите на кнопку ниже. 36 | """ 37 | 38 | main_menu = """ 39 | Главное меню 40 | """ 41 | 42 | user_not_defined = """ 43 | Пользователь не определен! 44 | Обратитесь к администратору, чтобы он добавил вас в базу данных. 45 | """ 46 | 47 | user_limit_exited = """ 48 | Ваш лимит на конфигурации исчерпан! 49 | """ 50 | 51 | config_id_await = """ 52 | Введите название конфигурации 53 | """ 54 | 55 | config_id_error = """ 56 | Ошибка! Введено некорректное название пользователя. 57 | Оно может содержать только латинские буквы и цифры. 58 | Чтобы вернуться, нажмите на кнопку ниже. 59 | """ 60 | 61 | config_is_deleted = """ 62 | Конфигурация удалена 63 | """ 64 | 65 | client_not_defined = """ 66 | Конфигурации не существует 67 | """ 68 | 69 | config_list = """ 70 | Список конфигураций 71 | """ 72 | 73 | configs_not_found = """ 74 | Конфигурации не найдены 75 | """ 76 | 77 | user_is_created = """ 78 | Пользователь добавлен! 79 | """ 80 | 81 | instruction_ios = """ 82 | Ссылка на инструкцию для iOS 83 | """ 84 | 85 | instruction_android = """ 86 | Ссылка на инструкцию для Android 87 | """ 88 | 89 | instruction_macos = """ 90 | Ссылка на инструкцию для MacOS 91 | """ 92 | 93 | instruction_windows = """ 94 | Ссылка на инструкцию для Windows 95 | """ 96 | --------------------------------------------------------------------------------