Repository: MHSanaei/3x-ui Branch: main Commit: 38d87230d326 Files: 206 Total size: 3.3 MB Directory structure: gitextract_ww701i7l/ ├── .github/ │ ├── FUNDING.yml │ ├── ISSUE_TEMPLATE/ │ │ ├── bug_report.yaml │ │ ├── feature_request.yaml │ │ └── question.yaml │ ├── copilot-instructions.md │ ├── dependabot.yml │ ├── pull_request_template.yml │ └── workflows/ │ ├── cleanup_caches.yml │ ├── docker.yml │ └── release.yml ├── .gitignore ├── CONTRIBUTING.md ├── DockerEntrypoint.sh ├── DockerInit.sh ├── Dockerfile ├── LICENSE ├── README.ar_EG.md ├── README.es_ES.md ├── README.fa_IR.md ├── README.md ├── README.ru_RU.md ├── README.zh_CN.md ├── config/ │ ├── config.go │ ├── name │ └── version ├── database/ │ ├── db.go │ └── model/ │ └── model.go ├── docker-compose.yml ├── go.mod ├── go.sum ├── install.sh ├── logger/ │ └── logger.go ├── main.go ├── sub/ │ ├── default.json │ ├── sub.go │ ├── subController.go │ ├── subJsonService.go │ └── subService.go ├── update.sh ├── util/ │ ├── common/ │ │ ├── err.go │ │ ├── format.go │ │ └── multi_error.go │ ├── crypto/ │ │ └── crypto.go │ ├── json_util/ │ │ └── json.go │ ├── ldap/ │ │ └── ldap.go │ ├── random/ │ │ └── random.go │ ├── reflect_util/ │ │ └── reflect.go │ └── sys/ │ ├── psutil.go │ ├── sys_darwin.go │ ├── sys_linux.go │ └── sys_windows.go ├── web/ │ ├── assets/ │ │ ├── codemirror/ │ │ │ ├── fold/ │ │ │ │ ├── brace-fold.js │ │ │ │ ├── foldcode.js │ │ │ │ ├── foldgutter.css │ │ │ │ └── foldgutter.js │ │ │ ├── hint/ │ │ │ │ └── javascript-hint.js │ │ │ ├── javascript.js │ │ │ ├── jshint.js │ │ │ ├── jsonlint.js │ │ │ └── lint/ │ │ │ ├── javascript-lint.js │ │ │ ├── lint.css │ │ │ └── lint.js │ │ └── js/ │ │ ├── axios-init.js │ │ ├── model/ │ │ │ ├── dbinbound.js │ │ │ ├── inbound.js │ │ │ ├── outbound.js │ │ │ ├── reality_targets.js │ │ │ └── setting.js │ │ ├── subscription.js │ │ ├── util/ │ │ │ └── index.js │ │ └── websocket.js │ ├── controller/ │ │ ├── api.go │ │ ├── base.go │ │ ├── inbound.go │ │ ├── index.go │ │ ├── server.go │ │ ├── setting.go │ │ ├── util.go │ │ ├── websocket.go │ │ ├── xray_setting.go │ │ └── xui.go │ ├── entity/ │ │ └── entity.go │ ├── global/ │ │ ├── global.go │ │ └── hashStorage.go │ ├── html/ │ │ ├── common/ │ │ │ └── page.html │ │ ├── component/ │ │ │ ├── aClientTable.html │ │ │ ├── aCustomStatistic.html │ │ │ ├── aPersianDatepicker.html │ │ │ ├── aSettingListItem.html │ │ │ ├── aSidebar.html │ │ │ ├── aTableSortable.html │ │ │ └── aThemeSwitch.html │ │ ├── form/ │ │ │ ├── client.html │ │ │ ├── inbound.html │ │ │ ├── outbound.html │ │ │ ├── protocol/ │ │ │ │ ├── dokodemo.html │ │ │ │ ├── http.html │ │ │ │ ├── shadowsocks.html │ │ │ │ ├── socks.html │ │ │ │ ├── trojan.html │ │ │ │ ├── tun.html │ │ │ │ ├── vless.html │ │ │ │ ├── vmess.html │ │ │ │ └── wireguard.html │ │ │ ├── reality_settings.html │ │ │ ├── sniffing.html │ │ │ ├── stream/ │ │ │ │ ├── external_proxy.html │ │ │ │ ├── stream_finalmask.html │ │ │ │ ├── stream_grpc.html │ │ │ │ ├── stream_httpupgrade.html │ │ │ │ ├── stream_kcp.html │ │ │ │ ├── stream_settings.html │ │ │ │ ├── stream_sockopt.html │ │ │ │ ├── stream_tcp.html │ │ │ │ ├── stream_ws.html │ │ │ │ └── stream_xhttp.html │ │ │ └── tls_settings.html │ │ ├── inbounds.html │ │ ├── index.html │ │ ├── login.html │ │ ├── modals/ │ │ │ ├── client_bulk_modal.html │ │ │ ├── client_modal.html │ │ │ ├── dns_presets_modal.html │ │ │ ├── inbound_info_modal.html │ │ │ ├── inbound_modal.html │ │ │ ├── prompt_modal.html │ │ │ ├── qrcode_modal.html │ │ │ ├── text_modal.html │ │ │ ├── two_factor_modal.html │ │ │ ├── warp_modal.html │ │ │ ├── xray_balancer_modal.html │ │ │ ├── xray_dns_modal.html │ │ │ ├── xray_fakedns_modal.html │ │ │ ├── xray_outbound_modal.html │ │ │ ├── xray_reverse_modal.html │ │ │ └── xray_rule_modal.html │ │ ├── settings/ │ │ │ ├── panel/ │ │ │ │ ├── general.html │ │ │ │ ├── security.html │ │ │ │ ├── subscription/ │ │ │ │ │ ├── general.html │ │ │ │ │ ├── json.html │ │ │ │ │ └── subpage.html │ │ │ │ └── telegram.html │ │ │ └── xray/ │ │ │ ├── advanced.html │ │ │ ├── balancers.html │ │ │ ├── basics.html │ │ │ ├── dns.html │ │ │ ├── outbounds.html │ │ │ ├── reverse.html │ │ │ └── routing.html │ │ ├── settings.html │ │ └── xray.html │ ├── job/ │ │ ├── check_client_ip_job.go │ │ ├── check_cpu_usage.go │ │ ├── check_hash_storage.go │ │ ├── check_xray_running_job.go │ │ ├── clear_logs_job.go │ │ ├── ldap_sync_job.go │ │ ├── periodic_traffic_reset_job.go │ │ ├── stats_notify_job.go │ │ └── xray_traffic_job.go │ ├── locale/ │ │ └── locale.go │ ├── middleware/ │ │ ├── domainValidator.go │ │ └── redirect.go │ ├── network/ │ │ ├── auto_https_conn.go │ │ └── auto_https_listener.go │ ├── service/ │ │ ├── config.json │ │ ├── inbound.go │ │ ├── outbound.go │ │ ├── panel.go │ │ ├── server.go │ │ ├── setting.go │ │ ├── tgbot.go │ │ ├── user.go │ │ ├── warp.go │ │ ├── xray.go │ │ └── xray_setting.go │ ├── session/ │ │ └── session.go │ ├── translation/ │ │ ├── translate.ar_EG.toml │ │ ├── translate.en_US.toml │ │ ├── translate.es_ES.toml │ │ ├── translate.fa_IR.toml │ │ ├── translate.id_ID.toml │ │ ├── translate.ja_JP.toml │ │ ├── translate.pt_BR.toml │ │ ├── translate.ru_RU.toml │ │ ├── translate.tr_TR.toml │ │ ├── translate.uk_UA.toml │ │ ├── translate.vi_VN.toml │ │ ├── translate.zh_CN.toml │ │ └── translate.zh_TW.toml │ ├── web.go │ └── websocket/ │ ├── hub.go │ └── notifier.go ├── windows_files/ │ └── readme.txt ├── x-ui.rc ├── x-ui.service.arch ├── x-ui.service.debian ├── x-ui.service.rhel ├── x-ui.sh └── xray/ ├── api.go ├── client_traffic.go ├── config.go ├── inbound.go ├── log_writer.go ├── process.go └── traffic.go ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/FUNDING.yml ================================================ # These are supported funding model platforms github: MHSanaei patreon: # Replace with a single Patreon username open_collective: # Replace with a single Open Collective username ko_fi: # Replace with a single Ko-fi username tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry liberapay: # Replace with a single Liberapay username issuehunt: # Replace with a single IssueHunt username lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry polar: # Replace with a single Polar username buy_me_a_coffee: mhsanaei custom: https://nowpayments.io/donation/hsanaei ================================================ FILE: .github/ISSUE_TEMPLATE/bug_report.yaml ================================================ name: Bug report description: Create a report to help us improve title: "Bug report" labels: ["bug"] body: - type: markdown attributes: value: | Thank you for reporting a bug! Please fill out the following information. - type: textarea id: what-happened attributes: label: Describe the bug description: A clear and concise description of what the bug is. placeholder: My problem is... validations: required: true - type: textarea id: how-repeat-problem attributes: label: How to repeat the problem? description: Sequence of actions that allow you to reproduce the bug placeholder: | 1. Open `Inbounds` page 2. ... validations: required: true - type: textarea id: expected-action attributes: label: Expected action description: What's going to happen placeholder: Must be... validations: required: false - type: textarea id: received-action attributes: label: Received action description: What's really happening placeholder: It's actually happening... validations: required: false - type: input id: xui-version attributes: label: 3x-ui Version description: Which version of 3x-ui are you using? placeholder: 2.X.X validations: required: true - type: input id: xray-version attributes: label: Xray-core Version description: Which version of Xray-core are you using? placeholder: 2.X.X validations: required: false - type: checkboxes id: checklist attributes: label: Checklist description: Please check all the checkboxes options: - label: This bug report is written entirely in English. required: true - label: This bug report is new and no one has reported it before me. required: true ================================================ FILE: .github/ISSUE_TEMPLATE/feature_request.yaml ================================================ name: Feature request description: Suggest an idea for this project title: "Feature request" labels: ["enhancement"] body: - type: textarea id: is-related-problem attributes: label: Is your feature request related to a problem? description: A clear and concise description of what the problem is. placeholder: I'm always frustrated when... validations: required: true - type: textarea id: solution attributes: label: Describe the solution you'd like description: A clear and concise description of what you want to happen. validations: required: true - type: textarea id: alternatives attributes: label: Describe alternatives you've considered description: A clear and concise description of any alternative solutions or features you've considered. validations: required: false - type: checkboxes id: checklist attributes: label: Checklist description: Please check all the checkboxes options: - label: This feature report is written entirely in English. required: true ================================================ FILE: .github/ISSUE_TEMPLATE/question.yaml ================================================ name: Question description: Describe this issue template's purpose here. title: "Question" labels: ["question"] body: - type: textarea id: question attributes: label: Question placeholder: I have a question, ..., how can I solve it? validations: required: true - type: checkboxes id: checklist attributes: label: Checklist description: Please check all the checkboxes options: - label: This question is written entirely in English. required: true ================================================ FILE: .github/copilot-instructions.md ================================================ # 3X-UI Development Guide ## Project Overview 3X-UI is a web-based control panel for managing Xray-core servers. It's a Go application using Gin web framework with embedded static assets and SQLite database. The panel manages VPN/proxy inbounds, monitors traffic, and provides Telegram bot integration. ## Architecture ### Core Components - **main.go**: Entry point that initializes database, web server, and subscription server. Handles graceful shutdown via SIGHUP/SIGTERM signals - **web/**: Primary web server with Gin router, HTML templates, and static assets embedded via `//go:embed` - **xray/**: Xray-core process management and API communication for traffic monitoring - **database/**: GORM-based SQLite database with models in `database/model/` - **sub/**: Subscription server running alongside main web server (separate port) - **web/service/**: Business logic layer containing InboundService, SettingService, TgBot, etc. - **web/controller/**: HTTP handlers using Gin context (`*gin.Context`) - **web/job/**: Cron-based background jobs for traffic monitoring, CPU checks, LDAP sync ### Key Architectural Patterns 1. **Embedded Resources**: All web assets (HTML, CSS, JS, translations) are embedded at compile time using `embed.FS`: - `web/assets` → `assetsFS` - `web/html` → `htmlFS` - `web/translation` → `i18nFS` 2. **Dual Server Design**: Main web panel + subscription server run concurrently, managed by `web/global` package 3. **Xray Integration**: Panel generates `config.json` for Xray binary, communicates via gRPC API for real-time traffic stats 4. **Signal-Based Restart**: SIGHUP triggers graceful restart. **Critical**: Always call `service.StopBot()` before restart to prevent Telegram bot 409 conflicts 5. **Database Seeders**: Uses `HistoryOfSeeders` model to track one-time migrations (e.g., password bcrypt migration) ## Development Workflows ### Building & Running ```bash # Build (creates bin/3x-ui.exe) go run tasks.json → "go: build" task # Run with debug logging XUI_DEBUG=true go run ./main.go # Or use task: "go: run" # Test go test ./... ``` ### Command-Line Operations The main.go accepts flags for admin tasks: - `-reset` - Reset all panel settings to defaults - `-show` - Display current settings (port, paths) - Use these by running the binary directly, not via web interface ### Database Management - DB path: Configured via `config.GetDBPath()`, typically `/etc/x-ui/x-ui.db` - Models: Located in `database/model/model.go` - Auto-migrated on startup - Seeders: Use `HistoryOfSeeders` to prevent re-running migrations - Default credentials: admin/admin (hashed with bcrypt) ### Telegram Bot Development - Bot instance in `web/service/tgbot.go` (3700+ lines) - Uses `telego` library with long polling - **Critical Pattern**: Must call `service.StopBot()` before any server restart to prevent 409 bot conflicts - Bot handlers use `telegohandler.BotHandler` for routing - i18n via embedded `i18nFS` passed to bot startup ## Code Conventions ### Service Layer Pattern Services inject dependencies (like xray.XrayAPI) and operate on GORM models: ```go type InboundService struct { xrayApi xray.XrayAPI } func (s *InboundService) GetInbounds(userId int) ([]*model.Inbound, error) { // Business logic here } ``` ### Controller Pattern Controllers use Gin context and inherit from BaseController: ```go func (a *InboundController) getInbounds(c *gin.Context) { // Use I18nWeb(c, "key") for translations // Check auth via checkLogin middleware } ``` ### Configuration Management - Environment vars: `XUI_DEBUG`, `XUI_LOG_LEVEL`, `XUI_MAIN_FOLDER` - Config embedded files: `config/version`, `config/name` - Use `config.GetLogLevel()`, `config.GetDBPath()` helpers ### Internationalization - Translation files: `web/translation/translate.*.toml` - Access via `I18nWeb(c, "pages.login.loginAgain")` in controllers - Use `locale.I18nType` enum (Web, Api, etc.) ## External Dependencies & Integration ### Xray-core - Binary management: Download platform-specific binary (`xray-{os}-{arch}`) to bin folder - Config generation: Panel creates `config.json` dynamically from inbound/outbound settings - Process control: Start/stop via `xray/process.go` - gRPC API: Real-time stats via `xray/api.go` using `google.golang.org/grpc` ### Critical External Paths - Xray binary: `{bin_folder}/xray-{os}-{arch}` - Xray config: `{bin_folder}/config.json` - GeoIP/GeoSite: `{bin_folder}/geoip.dat`, `geosite.dat` - Logs: `{log_folder}/3xipl.log`, `3xipl-banned.log` ### Job Scheduling Uses `robfig/cron/v3` for periodic tasks: - Traffic monitoring: `xray_traffic_job.go` - CPU alerts: `check_cpu_usage.go` - IP tracking: `check_client_ip_job.go` - LDAP sync: `ldap_sync_job.go` Jobs registered in `web/web.go` during server initialization ## Deployment & Scripts ### Installation Script Pattern Both `install.sh` and `x-ui.sh` follow these patterns: - Multi-distro support via `$release` variable (ubuntu, debian, centos, arch, etc.) - Port detection with `is_port_in_use()` using ss/netstat/lsof - Systemd service management with distro-specific unit files (`.service.debian`, `.service.arch`, `.service.rhel`) ### Docker Build Multi-stage Dockerfile: 1. **Builder**: CGO-enabled build, runs `DockerInit.sh` to download Xray binary 2. **Final**: Alpine-based with fail2ban pre-configured ### Key File Locations (Production) - Binary: `/usr/local/x-ui/` - Database: `/etc/x-ui/x-ui.db` - Logs: `/var/log/x-ui/` - Service: `/etc/systemd/system/x-ui.service.*` ## Testing & Debugging - Set `XUI_DEBUG=true` for detailed logging - Check Xray process: `x-ui.sh` script provides menu for status/logs - Database inspection: Direct SQLite access to x-ui.db - Traffic debugging: Check `3xipl.log` for IP limit tracking - Telegram bot: Logs show bot initialization and command handling ## Common Gotchas 1. **Bot Restart**: Always stop Telegram bot before server restart to avoid 409 conflict 2. **Embedded Assets**: Changes to HTML/CSS require recompilation (not hot-reload) 3. **Password Migration**: Seeder system tracks bcrypt migration - check `HistoryOfSeeders` table 4. **Port Binding**: Subscription server uses different port from main panel 5. **Xray Binary**: Must match OS/arch exactly - managed by installer scripts 6. **Session Management**: Uses `gin-contrib/sessions` with cookie store 7. **IP Limitation**: Implements "last IP wins" - when client exceeds LimitIP, oldest connections are automatically disconnected via Xray API to allow newest IPs ================================================ FILE: .github/dependabot.yml ================================================ # To get started with Dependabot version updates, you'll need to specify which # package ecosystems to update and where the package manifests are located. # Please see the documentation for all configuration options: # https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file version: 2 updates: - package-ecosystem: "github-actions" # See documentation for possible values directory: "/" # Location of package manifests schedule: interval: "weekly" ================================================ FILE: .github/pull_request_template.yml ================================================ ## What is the pull request? ## Which part of the application is affected by the change? - [ ] Frontend - [ ] Backend ## Type of Changes - [ ] Bug fix - [ ] New feature - [ ] Refactoring - [ ] Other ## Screenshots ================================================ FILE: .github/workflows/cleanup_caches.yml ================================================ name: Cleanup Caches on: schedule: - cron: '0 3 * * 0' # every Sunday workflow_dispatch: jobs: cleanup: runs-on: ubuntu-latest permissions: actions: write steps: - name: Delete caches older than 3 days env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | CUTOFF_DATE=$(date -d "3 days ago" -Ins --utc | sed 's/+0000/Z/') echo "Deleting caches older than: $CUTOFF_DATE" CACHE_IDS=$(gh api --paginate repos/${{ github.repository }}/actions/caches \ --jq ".actions_caches[] | select(.last_accessed_at < \"$CUTOFF_DATE\") | .id" 2>/dev/null) if [ -z "$CACHE_IDS" ]; then echo "No old caches found to delete." else echo "$CACHE_IDS" | while read CACHE_ID; do echo "Deleting cache: $CACHE_ID" gh api -X DELETE repos/${{ github.repository }}/actions/caches/$CACHE_ID done echo "Old caches deleted successfully." fi ================================================ FILE: .github/workflows/docker.yml ================================================ name: Release 3X-UI for Docker permissions: contents: read packages: write on: workflow_dispatch: push: tags: - "v*.*.*" jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 with: submodules: true - name: Docker meta id: meta uses: docker/metadata-action@v6 with: images: | hsanaeii/3x-ui ghcr.io/mhsanaei/3x-ui tags: | type=ref,event=branch type=ref,event=tag type=semver,pattern={{version}} - name: Set up QEMU uses: docker/setup-qemu-action@v4 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v4 with: install: true - name: Login to Docker Hub uses: docker/login-action@v4 with: username: ${{ secrets.DOCKER_HUB_USERNAME }} password: ${{ secrets.DOCKER_HUB_TOKEN }} - name: Login to GHCR uses: docker/login-action@v4 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - name: Build and push Docker image uses: docker/build-push-action@v7 with: context: . push: true platforms: linux/amd64,linux/arm64/v8,linux/arm/v7,linux/arm/v6,linux/386 tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} ================================================ FILE: .github/workflows/release.yml ================================================ name: Release 3X-UI on: workflow_dispatch: push: branches: - '**' tags: - "v*.*.*" paths: - '**.js' - '**.css' - '**.html' - '**.sh' - '**.go' - 'go.mod' - 'go.sum' - 'x-ui.service.debian' - 'x-ui.service.arch' - 'x-ui.service.rhel' pull_request: jobs: analyze: name: Analyze Go code permissions: contents: read runs-on: ubuntu-latest timeout-minutes: 20 steps: - name: Checkout repository uses: actions/checkout@v6 - name: Set up Go uses: actions/setup-go@v6 with: go-version-file: go.mod cache: true - name: Check formatting run: | unformatted=$(gofmt -l .) if [ -n "$unformatted" ]; then echo "These files are not gofmt-formatted:" echo "$unformatted" exit 1 fi - name: Run go vet run: go vet ./... - name: Run staticcheck uses: dominikh/staticcheck-action@v1 with: version: "latest" install-go: false - name: Run tests run: go test -race -shuffle=on ./... build: needs: analyze permissions: contents: write strategy: matrix: platform: - amd64 - arm64 - armv7 - armv6 - 386 - armv5 - s390x runs-on: ubuntu-latest steps: - name: Checkout repository uses: actions/checkout@v6 - name: Setup Go uses: actions/setup-go@v6 with: go-version-file: go.mod check-latest: true - name: Build 3X-UI run: | export CGO_ENABLED=1 export GOOS=linux export GOARCH=${{ matrix.platform }} # Use Bootlin prebuilt cross-toolchains (musl 1.2.5 in stable series) case "${{ matrix.platform }}" in amd64) BOOTLIN_ARCH="x86-64" ;; arm64) BOOTLIN_ARCH="aarch64" ;; armv7) BOOTLIN_ARCH="armv7-eabihf"; export GOARCH=arm GOARM=7 ;; armv6) BOOTLIN_ARCH="armv6-eabihf"; export GOARCH=arm GOARM=6 ;; armv5) BOOTLIN_ARCH="armv5-eabi"; export GOARCH=arm GOARM=5 ;; 386) BOOTLIN_ARCH="x86-i686" ;; s390x) BOOTLIN_ARCH="s390x-z13" ;; esac echo "Resolving Bootlin musl toolchain for arch=$BOOTLIN_ARCH (platform=${{ matrix.platform }})" TARBALL_BASE="https://toolchains.bootlin.com/downloads/releases/toolchains/$BOOTLIN_ARCH/tarballs/" TARBALL_URL=$(curl -fsSL "$TARBALL_BASE" | grep -oE "${BOOTLIN_ARCH}--musl--stable-[^\"]+\\.tar\\.xz" | sort -r | head -n1) [ -z "$TARBALL_URL" ] && { echo "Failed to locate Bootlin musl toolchain for arch=$BOOTLIN_ARCH" >&2; exit 1; } echo "Downloading: $TARBALL_URL" cd /tmp curl -fL -sS -o "$(basename "$TARBALL_URL")" "$TARBALL_BASE/$TARBALL_URL" tar -xf "$(basename "$TARBALL_URL")" TOOLCHAIN_DIR=$(find . -maxdepth 1 -type d -name "${BOOTLIN_ARCH}--musl--stable-*" | head -n1) export PATH="$(realpath "$TOOLCHAIN_DIR")/bin:$PATH" export CC=$(realpath "$(find "$TOOLCHAIN_DIR/bin" -name '*-gcc.br_real' -type f -executable | head -n1)") [ -z "$CC" ] && { echo "No gcc.br_real found in $TOOLCHAIN_DIR/bin" >&2; exit 1; } cd - go build -ldflags "-w -s -linkmode external -extldflags '-static'" -o xui-release -v main.go file xui-release ldd xui-release || echo "Static binary confirmed" mkdir x-ui cp xui-release x-ui/ cp x-ui.service.debian x-ui/ cp x-ui.service.arch x-ui/ cp x-ui.service.rhel x-ui/ cp x-ui.sh x-ui/ mv x-ui/xui-release x-ui/x-ui mkdir x-ui/bin cd x-ui/bin # Download dependencies Xray_URL="https://github.com/XTLS/Xray-core/releases/download/v26.2.6/" if [ "${{ matrix.platform }}" == "amd64" ]; then wget -q ${Xray_URL}Xray-linux-64.zip unzip Xray-linux-64.zip rm -f Xray-linux-64.zip elif [ "${{ matrix.platform }}" == "arm64" ]; then wget -q ${Xray_URL}Xray-linux-arm64-v8a.zip unzip Xray-linux-arm64-v8a.zip rm -f Xray-linux-arm64-v8a.zip elif [ "${{ matrix.platform }}" == "armv7" ]; then wget -q ${Xray_URL}Xray-linux-arm32-v7a.zip unzip Xray-linux-arm32-v7a.zip rm -f Xray-linux-arm32-v7a.zip elif [ "${{ matrix.platform }}" == "armv6" ]; then wget -q ${Xray_URL}Xray-linux-arm32-v6.zip unzip Xray-linux-arm32-v6.zip rm -f Xray-linux-arm32-v6.zip elif [ "${{ matrix.platform }}" == "386" ]; then wget -q ${Xray_URL}Xray-linux-32.zip unzip Xray-linux-32.zip rm -f Xray-linux-32.zip elif [ "${{ matrix.platform }}" == "armv5" ]; then wget -q ${Xray_URL}Xray-linux-arm32-v5.zip unzip Xray-linux-arm32-v5.zip rm -f Xray-linux-arm32-v5.zip elif [ "${{ matrix.platform }}" == "s390x" ]; then wget -q ${Xray_URL}Xray-linux-s390x.zip unzip Xray-linux-s390x.zip rm -f Xray-linux-s390x.zip fi rm -f geoip.dat geosite.dat wget -q https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geoip.dat wget -q https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geosite.dat wget -q -O geoip_IR.dat https://github.com/chocolate4u/Iran-v2ray-rules/releases/latest/download/geoip.dat wget -q -O geosite_IR.dat https://github.com/chocolate4u/Iran-v2ray-rules/releases/latest/download/geosite.dat wget -q -O geoip_RU.dat https://github.com/runetfreedom/russia-v2ray-rules-dat/releases/latest/download/geoip.dat wget -q -O geosite_RU.dat https://github.com/runetfreedom/russia-v2ray-rules-dat/releases/latest/download/geosite.dat mv xray xray-linux-${{ matrix.platform }} cd ../.. - name: Package run: tar -zcvf x-ui-linux-${{ matrix.platform }}.tar.gz x-ui - name: Upload files to Artifacts uses: actions/upload-artifact@v7 with: name: x-ui-linux-${{ matrix.platform }} path: ./x-ui-linux-${{ matrix.platform }}.tar.gz - name: Upload files to GH release uses: svenstaro/upload-release-action@v2 if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') with: repo_token: ${{ secrets.GITHUB_TOKEN }} tag: ${{ github.ref_name }} file: x-ui-linux-${{ matrix.platform }}.tar.gz asset_name: x-ui-linux-${{ matrix.platform }}.tar.gz overwrite: true prerelease: true # ================================= # Windows Build # ================================= build-windows: name: Build for Windows needs: analyze permissions: contents: write strategy: matrix: platform: - amd64 runs-on: windows-latest steps: - name: Checkout repository uses: actions/checkout@v6 - name: Setup Go uses: actions/setup-go@v6 with: go-version-file: go.mod check-latest: true - name: Install MSYS2 uses: msys2/setup-msys2@v2 with: msystem: MINGW64 update: true install: >- mingw-w64-x86_64-gcc mingw-w64-x86_64-sqlite3 mingw-w64-x86_64-pkg-config - name: Build 3X-UI for Windows (CGO) shell: msys2 {0} run: | export PATH="/c/hostedtoolcache/windows/go/$(ls /c/hostedtoolcache/windows/go | sort -V | tail -n1)/x64/bin:$PATH" export CGO_ENABLED=1 export GOOS=windows export GOARCH=amd64 export CC=x86_64-w64-mingw32-gcc which go go version gcc --version go build -ldflags "-w -s" -o xui-release.exe -v main.go - name: Copy and download resources shell: pwsh run: | mkdir x-ui Copy-Item xui-release.exe x-ui\x-ui.exe mkdir x-ui\bin cd x-ui\bin # Download Xray for Windows $Xray_URL = "https://github.com/XTLS/Xray-core/releases/download/v26.2.6/" Invoke-WebRequest -Uri "${Xray_URL}Xray-windows-64.zip" -OutFile "Xray-windows-64.zip" Expand-Archive -Path "Xray-windows-64.zip" -DestinationPath . Remove-Item "Xray-windows-64.zip" Remove-Item geoip.dat, geosite.dat -ErrorAction SilentlyContinue Invoke-WebRequest -Uri "https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geoip.dat" -OutFile "geoip.dat" Invoke-WebRequest -Uri "https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geosite.dat" -OutFile "geosite.dat" Invoke-WebRequest -Uri "https://github.com/chocolate4u/Iran-v2ray-rules/releases/latest/download/geoip.dat" -OutFile "geoip_IR.dat" Invoke-WebRequest -Uri "https://github.com/chocolate4u/Iran-v2ray-rules/releases/latest/download/geosite.dat" -OutFile "geosite_IR.dat" Invoke-WebRequest -Uri "https://github.com/runetfreedom/russia-v2ray-rules-dat/releases/latest/download/geoip.dat" -OutFile "geoip_RU.dat" Invoke-WebRequest -Uri "https://github.com/runetfreedom/russia-v2ray-rules-dat/releases/latest/download/geosite.dat" -OutFile "geosite_RU.dat" Rename-Item xray.exe xray-windows-amd64.exe cd .. Copy-Item -Path ..\windows_files\* -Destination . -Recurse cd .. - name: Package to Zip shell: pwsh run: | Compress-Archive -Path .\x-ui -DestinationPath "x-ui-windows-amd64.zip" - name: Upload files to Artifacts uses: actions/upload-artifact@v7 with: name: x-ui-windows-amd64 path: ./x-ui-windows-amd64.zip - name: Upload files to GH release uses: svenstaro/upload-release-action@v2 if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') with: repo_token: ${{ secrets.GITHUB_TOKEN }} tag: ${{ github.ref_name }} file: x-ui-windows-amd64.zip asset_name: x-ui-windows-amd64.zip overwrite: true prerelease: true ================================================ FILE: .gitignore ================================================ # Ignore editor and IDE settings .idea/ .vscode/ .cache/ .sync* # Ignore log files *.log # Ignore temporary files tmp/ *.tar.gz # Ignore build and distribution directories backup/ bin/ dist/ release/ node_modules/ # Ignore compiled binaries main # Ignore script and executable files /release.sh /x-ui # Ignore OS specific files .DS_Store Thumbs.db # Ignore Go build files *.exe x-ui.db # Ignore Docker specific files docker-compose.override.yml # Ignore .env (Environment Variables) file .env ================================================ FILE: CONTRIBUTING.md ================================================ ## Local Development Setup - Create a directory named `x-ui` in the project root - Rename `.env.example` to `.env ` - Run `main.go` ================================================ FILE: DockerEntrypoint.sh ================================================ #!/bin/sh # Start fail2ban [ $XUI_ENABLE_FAIL2BAN == "true" ] && fail2ban-client -x start # Run x-ui exec /app/x-ui ================================================ FILE: DockerInit.sh ================================================ #!/bin/sh case $1 in amd64) ARCH="64" FNAME="amd64" ;; i386) ARCH="32" FNAME="i386" ;; armv8 | arm64 | aarch64) ARCH="arm64-v8a" FNAME="arm64" ;; armv7 | arm | arm32) ARCH="arm32-v7a" FNAME="arm32" ;; armv6) ARCH="arm32-v6" FNAME="armv6" ;; *) ARCH="64" FNAME="amd64" ;; esac mkdir -p build/bin cd build/bin curl -sfLRO "https://github.com/XTLS/Xray-core/releases/download/v26.2.6/Xray-linux-${ARCH}.zip" unzip "Xray-linux-${ARCH}.zip" rm -f "Xray-linux-${ARCH}.zip" geoip.dat geosite.dat mv xray "xray-linux-${FNAME}" curl -sfLRO https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geoip.dat curl -sfLRO https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geosite.dat curl -sfLRo geoip_IR.dat https://github.com/chocolate4u/Iran-v2ray-rules/releases/latest/download/geoip.dat curl -sfLRo geosite_IR.dat https://github.com/chocolate4u/Iran-v2ray-rules/releases/latest/download/geosite.dat curl -sfLRo geoip_RU.dat https://github.com/runetfreedom/russia-v2ray-rules-dat/releases/latest/download/geoip.dat curl -sfLRo geosite_RU.dat https://github.com/runetfreedom/russia-v2ray-rules-dat/releases/latest/download/geosite.dat cd ../../ ================================================ FILE: Dockerfile ================================================ # ======================================================== # Stage: Builder # ======================================================== FROM golang:1.26-alpine AS builder WORKDIR /app ARG TARGETARCH RUN apk --no-cache --update add \ build-base \ gcc \ curl \ unzip COPY . . ENV CGO_ENABLED=1 ENV CGO_CFLAGS="-D_LARGEFILE64_SOURCE" RUN go build -ldflags "-w -s" -o build/x-ui main.go RUN ./DockerInit.sh "$TARGETARCH" # ======================================================== # Stage: Final Image of 3x-ui # ======================================================== FROM alpine ENV TZ=Asia/Tehran WORKDIR /app RUN apk add --no-cache --update \ ca-certificates \ tzdata \ fail2ban \ bash \ curl \ openssl COPY --from=builder /app/build/ /app/ COPY --from=builder /app/DockerEntrypoint.sh /app/ COPY --from=builder /app/x-ui.sh /usr/bin/x-ui # Configure fail2ban RUN rm -f /etc/fail2ban/jail.d/alpine-ssh.conf \ && cp /etc/fail2ban/jail.conf /etc/fail2ban/jail.local \ && sed -i "s/^\[ssh\]$/&\nenabled = false/" /etc/fail2ban/jail.local \ && sed -i "s/^\[sshd\]$/&\nenabled = false/" /etc/fail2ban/jail.local \ && sed -i "s/#allowipv6 = auto/allowipv6 = auto/g" /etc/fail2ban/fail2ban.conf RUN chmod +x \ /app/DockerEntrypoint.sh \ /app/x-ui \ /usr/bin/x-ui ENV XUI_ENABLE_FAIL2BAN="true" EXPOSE 2053 VOLUME [ "/etc/x-ui" ] CMD [ "./x-ui" ] ENTRYPOINT [ "/app/DockerEntrypoint.sh" ] ================================================ FILE: LICENSE ================================================ GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The GNU General Public License is a free, copyleft license for software and other kinds of works. The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. We, the Free Software Foundation, use the GNU General Public License for most of our software; it applies also to any other work released this way by its authors. You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. To protect your rights, we need to prevent others from denying you these rights or asking you to surrender the rights. Therefore, you have certain responsibilities if you distribute copies of the software, or if you modify it: responsibilities to respect the freedom of others. For example, if you distribute copies of such a program, whether gratis or for a fee, you must pass on to the recipients the same freedoms that you received. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. Developers that use the GNU GPL protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License giving you legal permission to copy, distribute and/or modify it. For the developers' and authors' protection, the GPL clearly explains that there is no warranty for this free software. For both users' and authors' sake, the GPL requires that modified versions be marked as changed, so that their problems will not be attributed erroneously to authors of previous versions. Some devices are designed to deny users access to install or run modified versions of the software inside them, although the manufacturer can do so. This is fundamentally incompatible with the aim of protecting users' freedom to change the software. The systematic pattern of such abuse occurs in the area of products for individuals to use, which is precisely where it is most unacceptable. Therefore, we have designed this version of the GPL to prohibit the practice for those products. If such problems arise substantially in other domains, we stand ready to extend this provision to those domains in future versions of the GPL, as needed to protect the freedom of users. Finally, every program is threatened constantly by software patents. States should not allow patents to restrict development and use of software on general-purpose computers, but in those that do, we wish to avoid the special danger that patents applied to a free program could make it effectively proprietary. To prevent this, the GPL assures that patents cannot be used to render the program non-free. The precise terms and conditions for copying, distribution and modification follow. TERMS AND CONDITIONS 0. Definitions. "This License" refers to version 3 of the GNU General Public License. "Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. "The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations. To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work. A "covered work" means either the unmodified Program or a work based on the Program. To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. 1. Source Code. The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work. A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work. The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. The Corresponding Source for a work in source code form is that same work. 2. Basic Permissions. All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. 3. Protecting Users' Legal Rights From Anti-Circumvention Law. No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. 4. Conveying Verbatim Copies. You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. 5. Conveying Modified Source Versions. You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: a) The work must carry prominent notices stating that you modified it, and giving a relevant date. b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices". c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. 6. Conveying Non-Source Forms. You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. "Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. 7. Additional Terms. "Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or d) Limiting the use for publicity purposes of names of licensors or authors of the material; or e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. 8. Termination. You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. 9. Acceptance Not Required for Having Copies. You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. 10. Automatic Licensing of Downstream Recipients. Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. 11. Patents. A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version". A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. 12. No Surrender of Others' Freedom. If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. 13. Use with the GNU Affero General Public License. Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU Affero General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the special requirements of the GNU Affero General Public License, section 13, concerning interaction through a network will apply to the combination as such. 14. Revised Versions of this License. The Free Software Foundation may publish revised and/or new versions of the GNU General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU General Public License, you may choose any version ever published by the Free Software Foundation. If the Program specifies that a proxy can decide which future versions of the GNU General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. 15. Disclaimer of Warranty. THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. Limitation of Liability. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 17. Interpretation of Sections 15 and 16. If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . Also add information on how to contact you by electronic and paper mail. If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode: Copyright (C) This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, your program's commands might be different; for a GUI interface, you would use an "about box". You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. For more information on this, and how to apply and follow the GNU GPL, see . The GNU General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. But first, please read . ================================================ FILE: README.ar_EG.md ================================================ [English](/README.md) | [فارسی](/README.fa_IR.md) | [العربية](/README.ar_EG.md) | [中文](/README.zh_CN.md) | [Español](/README.es_ES.md) | [Русский](/README.ru_RU.md)

3x-ui

[![Release](https://img.shields.io/github/v/release/mhsanaei/3x-ui.svg)](https://github.com/MHSanaei/3x-ui/releases) [![Build](https://img.shields.io/github/actions/workflow/status/mhsanaei/3x-ui/release.yml.svg)](https://github.com/MHSanaei/3x-ui/actions) [![GO Version](https://img.shields.io/github/go-mod/go-version/mhsanaei/3x-ui.svg)](#) [![Downloads](https://img.shields.io/github/downloads/mhsanaei/3x-ui/total.svg)](https://github.com/MHSanaei/3x-ui/releases/latest) [![License](https://img.shields.io/badge/license-GPL%20V3-blue.svg?longCache=true)](https://www.gnu.org/licenses/gpl-3.0.en.html) [![Go Reference](https://pkg.go.dev/badge/github.com/mhsanaei/3x-ui/v2.svg)](https://pkg.go.dev/github.com/mhsanaei/3x-ui/v2) [![Go Report Card](https://goreportcard.com/badge/github.com/mhsanaei/3x-ui/v2)](https://goreportcard.com/report/github.com/mhsanaei/3x-ui/v2) **3X-UI** — لوحة تحكم متقدمة مفتوحة المصدر تعتمد على الويب مصممة لإدارة خادم Xray-core. توفر واجهة سهلة الاستخدام لتكوين ومراقبة بروتوكولات VPN والوكيل المختلفة. > [!IMPORTANT] > هذا المشروع مخصص للاستخدام الشخصي والاتصال فقط، يرجى عدم استخدامه لأغراض غير قانونية، يرجى عدم استخدامه في بيئة الإنتاج. كمشروع محسن من مشروع X-UI الأصلي، يوفر 3X-UI استقرارًا محسنًا ودعمًا أوسع للبروتوكولات وميزات إضافية. ## البدء السريع ``` bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.sh) ``` للحصول على الوثائق الكاملة، يرجى زيارة [ويكي المشروع](https://github.com/MHSanaei/3x-ui/wiki). ## شكر خاص إلى - [alireza0](https://github.com/alireza0/) ## الاعتراف - [Iran v2ray rules](https://github.com/chocolate4u/Iran-v2ray-rules) (الترخيص: **GPL-3.0**): _قواعد توجيه v2ray/xray و v2ray/xray-clients المحسنة مع النطاقات الإيرانية المدمجة وتركيز على الأمان وحظر الإعلانات._ - [Russia v2ray rules](https://github.com/runetfreedom/russia-v2ray-rules-dat) (الترخيص: **GPL-3.0**): _يحتوي هذا المستودع على قواعد توجيه V2Ray محدثة تلقائيًا بناءً على بيانات النطاقات والعناوين المحظورة في روسيا._ ## دعم المشروع **إذا كان هذا المشروع مفيدًا لك، فقد ترغب في إعطائه**:star2: Buy Me A Coffee
Crypto donation button by NOWPayments ## النجوم عبر الزمن [![Stargazers over time](https://starchart.cc/MHSanaei/3x-ui.svg?variant=adaptive)](https://starchart.cc/MHSanaei/3x-ui) ================================================ FILE: README.es_ES.md ================================================ [English](/README.md) | [فارسی](/README.fa_IR.md) | [العربية](/README.ar_EG.md) | [中文](/README.zh_CN.md) | [Español](/README.es_ES.md) | [Русский](/README.ru_RU.md)

3x-ui

[![Release](https://img.shields.io/github/v/release/mhsanaei/3x-ui.svg)](https://github.com/MHSanaei/3x-ui/releases) [![Build](https://img.shields.io/github/actions/workflow/status/mhsanaei/3x-ui/release.yml.svg)](https://github.com/MHSanaei/3x-ui/actions) [![GO Version](https://img.shields.io/github/go-mod/go-version/mhsanaei/3x-ui.svg)](#) [![Downloads](https://img.shields.io/github/downloads/mhsanaei/3x-ui/total.svg)](https://github.com/MHSanaei/3x-ui/releases/latest) [![License](https://img.shields.io/badge/license-GPL%20V3-blue.svg?longCache=true)](https://www.gnu.org/licenses/gpl-3.0.en.html) [![Go Reference](https://pkg.go.dev/badge/github.com/mhsanaei/3x-ui/v2.svg)](https://pkg.go.dev/github.com/mhsanaei/3x-ui/v2) [![Go Report Card](https://goreportcard.com/badge/github.com/mhsanaei/3x-ui/v2)](https://goreportcard.com/report/github.com/mhsanaei/3x-ui/v2) **3X-UI** — panel de control avanzado basado en web de código abierto diseñado para gestionar el servidor Xray-core. Ofrece una interfaz fácil de usar para configurar y monitorear varios protocolos VPN y proxy. > [!IMPORTANT] > Este proyecto es solo para uso personal y comunicación, por favor no lo use para fines ilegales, por favor no lo use en un entorno de producción. Como una versión mejorada del proyecto X-UI original, 3X-UI proporciona mayor estabilidad, soporte más amplio de protocolos y características adicionales. ## Inicio Rápido ``` bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.sh) ``` Para documentación completa, visita la [Wiki del proyecto](https://github.com/MHSanaei/3x-ui/wiki). ## Un Agradecimiento Especial a - [alireza0](https://github.com/alireza0/) ## Reconocimientos - [Iran v2ray rules](https://github.com/chocolate4u/Iran-v2ray-rules) (Licencia: **GPL-3.0**): _Reglas de enrutamiento mejoradas para v2ray/xray y v2ray/xray-clients con dominios iraníes incorporados y un enfoque en seguridad y bloqueo de anuncios._ - [Russia v2ray rules](https://github.com/runetfreedom/russia-v2ray-rules-dat) (Licencia: **GPL-3.0**): _Este repositorio contiene reglas de enrutamiento V2Ray actualizadas automáticamente basadas en datos de dominios y direcciones bloqueadas en Rusia._ ## Apoyar el Proyecto **Si este proyecto te es útil, puedes darle una**:star2: Buy Me A Coffee
Crypto donation button by NOWPayments ## Estrellas a lo Largo del Tiempo [![Stargazers over time](https://starchart.cc/MHSanaei/3x-ui.svg?variant=adaptive)](https://starchart.cc/MHSanaei/3x-ui) ================================================ FILE: README.fa_IR.md ================================================ [English](/README.md) | [فارسی](/README.fa_IR.md) | [العربية](/README.ar_EG.md) | [中文](/README.zh_CN.md) | [Español](/README.es_ES.md) | [Русский](/README.ru_RU.md)

3x-ui

[![Release](https://img.shields.io/github/v/release/mhsanaei/3x-ui.svg)](https://github.com/MHSanaei/3x-ui/releases) [![Build](https://img.shields.io/github/actions/workflow/status/mhsanaei/3x-ui/release.yml.svg)](https://github.com/MHSanaei/3x-ui/actions) [![GO Version](https://img.shields.io/github/go-mod/go-version/mhsanaei/3x-ui.svg)](#) [![Downloads](https://img.shields.io/github/downloads/mhsanaei/3x-ui/total.svg)](https://github.com/MHSanaei/3x-ui/releases/latest) [![License](https://img.shields.io/badge/license-GPL%20V3-blue.svg?longCache=true)](https://www.gnu.org/licenses/gpl-3.0.en.html) [![Go Reference](https://pkg.go.dev/badge/github.com/mhsanaei/3x-ui/v2.svg)](https://pkg.go.dev/github.com/mhsanaei/3x-ui/v2) [![Go Report Card](https://goreportcard.com/badge/github.com/mhsanaei/3x-ui/v2)](https://goreportcard.com/report/github.com/mhsanaei/3x-ui/v2) **3X-UI** — یک پنل کنترل پیشرفته مبتنی بر وب با کد باز که برای مدیریت سرور Xray-core طراحی شده است. این پنل یک رابط کاربری آسان برای پیکربندی و نظارت بر پروتکل‌های مختلف VPN و پراکسی ارائه می‌دهد. > [!IMPORTANT] > این پروژه فقط برای استفاده شخصی و ارتباطات است، لطفاً از آن برای اهداف غیرقانونی استفاده نکنید، لطفاً از آن در محیط تولید استفاده نکنید. به عنوان یک نسخه بهبود یافته از پروژه اصلی X-UI، 3X-UI پایداری بهتر، پشتیبانی گسترده‌تر از پروتکل‌ها و ویژگی‌های اضافی را ارائه می‌دهد. ## شروع سریع ``` bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.sh) ``` برای مستندات کامل، لطفاً به [ویکی پروژه](https://github.com/MHSanaei/3x-ui/wiki) مراجعه کنید. ## تشکر ویژه از - [alireza0](https://github.com/alireza0/) ## قدردانی - [Iran v2ray rules](https://github.com/chocolate4u/Iran-v2ray-rules) (مجوز: **GPL-3.0**): _قوانین مسیریابی بهبود یافته v2ray/xray و v2ray/xray-clients با دامنه‌های ایرانی داخلی و تمرکز بر امنیت و مسدود کردن تبلیغات._ - [Russia v2ray rules](https://github.com/runetfreedom/russia-v2ray-rules-dat) (مجوز: **GPL-3.0**): _این مخزن شامل قوانین مسیریابی V2Ray به‌روزرسانی شده خودکار بر اساس داده‌های دامنه‌ها و آدرس‌های مسدود شده در روسیه است._ ## پشتیبانی از پروژه **اگر این پروژه برای شما مفید است، می‌توانید به آن یک**:star2: بدهید Buy Me A Coffee
Crypto donation button by NOWPayments ## ستاره‌ها در طول زمان [![Stargazers over time](https://starchart.cc/MHSanaei/3x-ui.svg?variant=adaptive)](https://starchart.cc/MHSanaei/3x-ui) ================================================ FILE: README.md ================================================ [English](/README.md) | [فارسی](/README.fa_IR.md) | [العربية](/README.ar_EG.md) | [中文](/README.zh_CN.md) | [Español](/README.es_ES.md) | [Русский](/README.ru_RU.md)

3x-ui

[![Release](https://img.shields.io/github/v/release/mhsanaei/3x-ui.svg)](https://github.com/MHSanaei/3x-ui/releases) [![Build](https://img.shields.io/github/actions/workflow/status/mhsanaei/3x-ui/release.yml.svg)](https://github.com/MHSanaei/3x-ui/actions) [![GO Version](https://img.shields.io/github/go-mod/go-version/mhsanaei/3x-ui.svg)](#) [![Downloads](https://img.shields.io/github/downloads/mhsanaei/3x-ui/total.svg)](https://github.com/MHSanaei/3x-ui/releases/latest) [![License](https://img.shields.io/badge/license-GPL%20V3-blue.svg?longCache=true)](https://www.gnu.org/licenses/gpl-3.0.en.html) [![Go Reference](https://pkg.go.dev/badge/github.com/mhsanaei/3x-ui/v2.svg)](https://pkg.go.dev/github.com/mhsanaei/3x-ui/v2) [![Go Report Card](https://goreportcard.com/badge/github.com/mhsanaei/3x-ui/v2)](https://goreportcard.com/report/github.com/mhsanaei/3x-ui/v2) **3X-UI** — advanced, open-source web-based control panel designed for managing Xray-core server. It offers a user-friendly interface for configuring and monitoring various VPN and proxy protocols. > [!IMPORTANT] > This project is only for personal usage, please do not use it for illegal purposes, and please do not use it in a production environment. As an enhanced fork of the original X-UI project, 3X-UI provides improved stability, broader protocol support, and additional features. ## Quick Start ```bash bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.sh) ``` For full documentation, please visit the [project Wiki](https://github.com/MHSanaei/3x-ui/wiki). ## A Special Thanks to - [alireza0](https://github.com/alireza0/) ## Acknowledgment - [Iran v2ray rules](https://github.com/chocolate4u/Iran-v2ray-rules) (License: **GPL-3.0**): _Enhanced v2ray/xray and v2ray/xray-clients routing rules with built-in Iranian domains and a focus on security and adblocking._ - [Russia v2ray rules](https://github.com/runetfreedom/russia-v2ray-rules-dat) (License: **GPL-3.0**): _This repository contains automatically updated V2Ray routing rules based on data on blocked domains and addresses in Russia._ ## Support project **If this project is helpful to you, you may wish to give it a**:star2: Buy Me A Coffee
Crypto donation button by NOWPayments ## Stargazers over Time [![Stargazers over time](https://starchart.cc/MHSanaei/3x-ui.svg?variant=adaptive)](https://starchart.cc/MHSanaei/3x-ui) ================================================ FILE: README.ru_RU.md ================================================ [English](/README.md) | [فارسی](/README.fa_IR.md) | [العربية](/README.ar_EG.md) | [中文](/README.zh_CN.md) | [Español](/README.es_ES.md) | [Русский](/README.ru_RU.md)

3x-ui

[![Release](https://img.shields.io/github/v/release/mhsanaei/3x-ui.svg)](https://github.com/MHSanaei/3x-ui/releases) [![Build](https://img.shields.io/github/actions/workflow/status/mhsanaei/3x-ui/release.yml.svg)](https://github.com/MHSanaei/3x-ui/actions) [![GO Version](https://img.shields.io/github/go-mod/go-version/mhsanaei/3x-ui.svg)](#) [![Downloads](https://img.shields.io/github/downloads/mhsanaei/3x-ui/total.svg)](https://github.com/MHSanaei/3x-ui/releases/latest) [![License](https://img.shields.io/badge/license-GPL%20V3-blue.svg?longCache=true)](https://www.gnu.org/licenses/gpl-3.0.en.html) [![Go Reference](https://pkg.go.dev/badge/github.com/mhsanaei/3x-ui/v2.svg)](https://pkg.go.dev/github.com/mhsanaei/3x-ui/v2) [![Go Report Card](https://goreportcard.com/badge/github.com/mhsanaei/3x-ui/v2)](https://goreportcard.com/report/github.com/mhsanaei/3x-ui/v2) **3X-UI** — продвинутая панель управления с открытым исходным кодом на основе веб-интерфейса, разработанная для управления сервером Xray-core. Предоставляет удобный интерфейс для настройки и мониторинга различных VPN и прокси-протоколов. > [!IMPORTANT] > Этот проект предназначен только для личного использования, пожалуйста, не используйте его в незаконных целях и в производственной среде. Как улучшенная версия оригинального проекта X-UI, 3X-UI обеспечивает повышенную стабильность, более широкую поддержку протоколов и дополнительные функции. ## Быстрый старт ``` bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.sh) ``` Полную документацию смотрите в [вики проекта](https://github.com/MHSanaei/3x-ui/wiki). ## Особая благодарность - [alireza0](https://github.com/alireza0/) ## Благодарности - [Iran v2ray rules](https://github.com/chocolate4u/Iran-v2ray-rules) (Лицензия: **GPL-3.0**): _Улучшенные правила маршрутизации для v2ray/xray и v2ray/xray-clients со встроенными иранскими доменами и фокусом на безопасность и блокировку рекламы._ - [Russia v2ray rules](https://github.com/runetfreedom/russia-v2ray-rules-dat) (Лицензия: **GPL-3.0**): _Этот репозиторий содержит автоматически обновляемые правила маршрутизации V2Ray на основе данных о заблокированных доменах и адресах в России._ ## Поддержка проекта **Если этот проект полезен для вас, вы можете поставить ему**:star2: Buy Me A Coffee
Crypto donation button by NOWPayments ## Звезды с течением времени [![Stargazers over time](https://starchart.cc/MHSanaei/3x-ui.svg?variant=adaptive)](https://starchart.cc/MHSanaei/3x-ui) ================================================ FILE: README.zh_CN.md ================================================ [English](/README.md) | [فارسی](/README.fa_IR.md) | [العربية](/README.ar_EG.md) | [中文](/README.zh_CN.md) | [Español](/README.es_ES.md) | [Русский](/README.ru_RU.md)

3x-ui

[![Release](https://img.shields.io/github/v/release/mhsanaei/3x-ui.svg)](https://github.com/MHSanaei/3x-ui/releases) [![Build](https://img.shields.io/github/actions/workflow/status/mhsanaei/3x-ui/release.yml.svg)](https://github.com/MHSanaei/3x-ui/actions) [![GO Version](https://img.shields.io/github/go-mod/go-version/mhsanaei/3x-ui.svg)](#) [![Downloads](https://img.shields.io/github/downloads/mhsanaei/3x-ui/total.svg)](https://github.com/MHSanaei/3x-ui/releases/latest) [![License](https://img.shields.io/badge/license-GPL%20V3-blue.svg?longCache=true)](https://www.gnu.org/licenses/gpl-3.0.en.html) [![Go Reference](https://pkg.go.dev/badge/github.com/mhsanaei/3x-ui/v2.svg)](https://pkg.go.dev/github.com/mhsanaei/3x-ui/v2) [![Go Report Card](https://goreportcard.com/badge/github.com/mhsanaei/3x-ui/v2)](https://goreportcard.com/report/github.com/mhsanaei/3x-ui/v2) **3X-UI** — 一个基于网页的高级开源控制面板,专为管理 Xray-core 服务器而设计。它提供了用户友好的界面,用于配置和监控各种 VPN 和代理协议。 > [!IMPORTANT] > 本项目仅用于个人使用和通信,请勿将其用于非法目的,请勿在生产环境中使用。 作为原始 X-UI 项目的增强版本,3X-UI 提供了更好的稳定性、更广泛的协议支持和额外的功能。 ## 快速开始 ``` bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.sh) ``` 完整文档请参阅 [项目Wiki](https://github.com/MHSanaei/3x-ui/wiki)。 ## 特别感谢 - [alireza0](https://github.com/alireza0/) ## 致谢 - [Iran v2ray rules](https://github.com/chocolate4u/Iran-v2ray-rules) (许可证: **GPL-3.0**): _增强的 v2ray/xray 和 v2ray/xray-clients 路由规则,内置伊朗域名,专注于安全性和广告拦截。_ - [Russia v2ray rules](https://github.com/runetfreedom/russia-v2ray-rules-dat) (许可证: **GPL-3.0**): _此仓库包含基于俄罗斯被阻止域名和地址数据自动更新的 V2Ray 路由规则。_ ## 支持项目 **如果这个项目对您有帮助,您可以给它一个**:star2: Buy Me A Coffee
Crypto donation button by NOWPayments ## 随时间变化的星标数 [![Stargazers over time](https://starchart.cc/MHSanaei/3x-ui.svg?variant=adaptive)](https://starchart.cc/MHSanaei/3x-ui) ================================================ FILE: config/config.go ================================================ // Package config provides configuration management utilities for the 3x-ui panel, // including version information, logging levels, database paths, and environment variable handling. package config import ( _ "embed" "fmt" "io" "os" "path/filepath" "runtime" "strings" ) //go:embed version var version string //go:embed name var name string // LogLevel represents the logging level for the application. type LogLevel string // Logging level constants const ( Debug LogLevel = "debug" Info LogLevel = "info" Notice LogLevel = "notice" Warning LogLevel = "warning" Error LogLevel = "error" ) // GetVersion returns the version string of the 3x-ui application. func GetVersion() string { return strings.TrimSpace(version) } // GetName returns the name of the 3x-ui application. func GetName() string { return strings.TrimSpace(name) } // GetLogLevel returns the current logging level based on environment variables or defaults to Info. func GetLogLevel() LogLevel { if IsDebug() { return Debug } logLevel := os.Getenv("XUI_LOG_LEVEL") if logLevel == "" { return Info } return LogLevel(logLevel) } // IsDebug returns true if debug mode is enabled via the XUI_DEBUG environment variable. func IsDebug() bool { return os.Getenv("XUI_DEBUG") == "true" } // GetBinFolderPath returns the path to the binary folder, defaulting to "bin" if not set via XUI_BIN_FOLDER. func GetBinFolderPath() string { binFolderPath := os.Getenv("XUI_BIN_FOLDER") if binFolderPath == "" { binFolderPath = "bin" } return binFolderPath } func getBaseDir() string { exePath, err := os.Executable() if err != nil { return "." } exeDir := filepath.Dir(exePath) exeDirLower := strings.ToLower(filepath.ToSlash(exeDir)) if strings.Contains(exeDirLower, "/appdata/local/temp/") || strings.Contains(exeDirLower, "/go-build") { wd, err := os.Getwd() if err != nil { return "." } return wd } return exeDir } // GetDBFolderPath returns the path to the database folder based on environment variables or platform defaults. func GetDBFolderPath() string { dbFolderPath := os.Getenv("XUI_DB_FOLDER") if dbFolderPath != "" { return dbFolderPath } if runtime.GOOS == "windows" { return getBaseDir() } return "/etc/x-ui" } // GetDBPath returns the full path to the database file. func GetDBPath() string { return fmt.Sprintf("%s/%s.db", GetDBFolderPath(), GetName()) } // GetLogFolder returns the path to the log folder based on environment variables or platform defaults. func GetLogFolder() string { logFolderPath := os.Getenv("XUI_LOG_FOLDER") if logFolderPath != "" { return logFolderPath } if runtime.GOOS == "windows" { return filepath.Join(".", "log") } return "/var/log/x-ui" } func copyFile(src, dst string) error { in, err := os.Open(src) if err != nil { return err } defer in.Close() out, err := os.Create(dst) if err != nil { return err } defer out.Close() _, err = io.Copy(out, in) if err != nil { return err } return out.Sync() } func init() { if runtime.GOOS != "windows" { return } if os.Getenv("XUI_DB_FOLDER") != "" { return } oldDBFolder := "/etc/x-ui" oldDBPath := fmt.Sprintf("%s/%s.db", oldDBFolder, GetName()) newDBFolder := GetDBFolderPath() newDBPath := fmt.Sprintf("%s/%s.db", newDBFolder, GetName()) _, err := os.Stat(newDBPath) if err == nil { return // new exists } _, err = os.Stat(oldDBPath) if os.IsNotExist(err) { return // old does not exist } _ = copyFile(oldDBPath, newDBPath) // ignore error } ================================================ FILE: config/name ================================================ x-ui ================================================ FILE: config/version ================================================ 2.8.11 ================================================ FILE: database/db.go ================================================ // Package database provides database initialization, migration, and management utilities // for the 3x-ui panel using GORM with SQLite. package database import ( "bytes" "errors" "io" "io/fs" "log" "os" "path" "slices" "github.com/mhsanaei/3x-ui/v2/config" "github.com/mhsanaei/3x-ui/v2/database/model" "github.com/mhsanaei/3x-ui/v2/util/crypto" "github.com/mhsanaei/3x-ui/v2/xray" "gorm.io/driver/sqlite" "gorm.io/gorm" "gorm.io/gorm/logger" ) var db *gorm.DB const ( defaultUsername = "admin" defaultPassword = "admin" ) func initModels() error { models := []any{ &model.User{}, &model.Inbound{}, &model.OutboundTraffics{}, &model.Setting{}, &model.InboundClientIps{}, &xray.ClientTraffic{}, &model.HistoryOfSeeders{}, } for _, model := range models { if err := db.AutoMigrate(model); err != nil { log.Printf("Error auto migrating model: %v", err) return err } } return nil } // initUser creates a default admin user if the users table is empty. func initUser() error { empty, err := isTableEmpty("users") if err != nil { log.Printf("Error checking if users table is empty: %v", err) return err } if empty { hashedPassword, err := crypto.HashPasswordAsBcrypt(defaultPassword) if err != nil { log.Printf("Error hashing default password: %v", err) return err } user := &model.User{ Username: defaultUsername, Password: hashedPassword, } return db.Create(user).Error } return nil } // runSeeders migrates user passwords to bcrypt and records seeder execution to prevent re-running. func runSeeders(isUsersEmpty bool) error { empty, err := isTableEmpty("history_of_seeders") if err != nil { log.Printf("Error checking if users table is empty: %v", err) return err } if empty && isUsersEmpty { hashSeeder := &model.HistoryOfSeeders{ SeederName: "UserPasswordHash", } return db.Create(hashSeeder).Error } else { var seedersHistory []string db.Model(&model.HistoryOfSeeders{}).Pluck("seeder_name", &seedersHistory) if !slices.Contains(seedersHistory, "UserPasswordHash") && !isUsersEmpty { var users []model.User db.Find(&users) for _, user := range users { hashedPassword, err := crypto.HashPasswordAsBcrypt(user.Password) if err != nil { log.Printf("Error hashing password for user '%s': %v", user.Username, err) return err } db.Model(&user).Update("password", hashedPassword) } hashSeeder := &model.HistoryOfSeeders{ SeederName: "UserPasswordHash", } return db.Create(hashSeeder).Error } } return nil } // isTableEmpty returns true if the named table contains zero rows. func isTableEmpty(tableName string) (bool, error) { var count int64 err := db.Table(tableName).Count(&count).Error return count == 0, err } // InitDB sets up the database connection, migrates models, and runs seeders. func InitDB(dbPath string) error { dir := path.Dir(dbPath) err := os.MkdirAll(dir, fs.ModePerm) if err != nil { return err } var gormLogger logger.Interface if config.IsDebug() { gormLogger = logger.Default } else { gormLogger = logger.Discard } c := &gorm.Config{ Logger: gormLogger, } db, err = gorm.Open(sqlite.Open(dbPath), c) if err != nil { return err } if err := initModels(); err != nil { return err } isUsersEmpty, err := isTableEmpty("users") if err != nil { return err } if err := initUser(); err != nil { return err } return runSeeders(isUsersEmpty) } // CloseDB closes the database connection if it exists. func CloseDB() error { if db != nil { sqlDB, err := db.DB() if err != nil { return err } return sqlDB.Close() } return nil } // GetDB returns the global GORM database instance. func GetDB() *gorm.DB { return db } // IsNotFound checks if the given error is a GORM record not found error. func IsNotFound(err error) bool { return err == gorm.ErrRecordNotFound } // IsSQLiteDB checks if the given file is a valid SQLite database by reading its signature. func IsSQLiteDB(file io.ReaderAt) (bool, error) { signature := []byte("SQLite format 3\x00") buf := make([]byte, len(signature)) _, err := file.ReadAt(buf, 0) if err != nil { return false, err } return bytes.Equal(buf, signature), nil } // Checkpoint performs a WAL checkpoint on the SQLite database to ensure data consistency. func Checkpoint() error { // Update WAL err := db.Exec("PRAGMA wal_checkpoint;").Error if err != nil { return err } return nil } // ValidateSQLiteDB opens the provided sqlite DB path with a throw-away connection // and runs a PRAGMA integrity_check to ensure the file is structurally sound. // It does not mutate global state or run migrations. func ValidateSQLiteDB(dbPath string) error { if _, err := os.Stat(dbPath); err != nil { // file must exist return err } gdb, err := gorm.Open(sqlite.Open(dbPath), &gorm.Config{Logger: logger.Discard}) if err != nil { return err } sqlDB, err := gdb.DB() if err != nil { return err } defer sqlDB.Close() var res string if err := gdb.Raw("PRAGMA integrity_check;").Scan(&res).Error; err != nil { return err } if res != "ok" { return errors.New("sqlite integrity check failed: " + res) } return nil } ================================================ FILE: database/model/model.go ================================================ // Package model defines the database models and data structures used by the 3x-ui panel. package model import ( "fmt" "github.com/mhsanaei/3x-ui/v2/util/json_util" "github.com/mhsanaei/3x-ui/v2/xray" ) // Protocol represents the protocol type for Xray inbounds. type Protocol string // Protocol constants for different Xray inbound protocols const ( VMESS Protocol = "vmess" VLESS Protocol = "vless" Tunnel Protocol = "tunnel" HTTP Protocol = "http" Trojan Protocol = "trojan" Shadowsocks Protocol = "shadowsocks" Mixed Protocol = "mixed" WireGuard Protocol = "wireguard" ) // User represents a user account in the 3x-ui panel. type User struct { Id int `json:"id" gorm:"primaryKey;autoIncrement"` Username string `json:"username"` Password string `json:"password"` } // Inbound represents an Xray inbound configuration with traffic statistics and settings. type Inbound struct { Id int `json:"id" form:"id" gorm:"primaryKey;autoIncrement"` // Unique identifier UserId int `json:"-"` // Associated user ID Up int64 `json:"up" form:"up"` // Upload traffic in bytes Down int64 `json:"down" form:"down"` // Download traffic in bytes Total int64 `json:"total" form:"total"` // Total traffic limit in bytes AllTime int64 `json:"allTime" form:"allTime" gorm:"default:0"` // All-time traffic usage Remark string `json:"remark" form:"remark"` // Human-readable remark Enable bool `json:"enable" form:"enable" gorm:"index:idx_enable_traffic_reset,priority:1"` // Whether the inbound is enabled ExpiryTime int64 `json:"expiryTime" form:"expiryTime"` // Expiration timestamp TrafficReset string `json:"trafficReset" form:"trafficReset" gorm:"default:never;index:idx_enable_traffic_reset,priority:2"` // Traffic reset schedule LastTrafficResetTime int64 `json:"lastTrafficResetTime" form:"lastTrafficResetTime" gorm:"default:0"` // Last traffic reset timestamp ClientStats []xray.ClientTraffic `gorm:"foreignKey:InboundId;references:Id" json:"clientStats" form:"clientStats"` // Client traffic statistics // Xray configuration fields Listen string `json:"listen" form:"listen"` Port int `json:"port" form:"port"` Protocol Protocol `json:"protocol" form:"protocol"` Settings string `json:"settings" form:"settings"` StreamSettings string `json:"streamSettings" form:"streamSettings"` Tag string `json:"tag" form:"tag" gorm:"unique"` Sniffing string `json:"sniffing" form:"sniffing"` } // OutboundTraffics tracks traffic statistics for Xray outbound connections. type OutboundTraffics struct { Id int `json:"id" form:"id" gorm:"primaryKey;autoIncrement"` Tag string `json:"tag" form:"tag" gorm:"unique"` Up int64 `json:"up" form:"up" gorm:"default:0"` Down int64 `json:"down" form:"down" gorm:"default:0"` Total int64 `json:"total" form:"total" gorm:"default:0"` } // InboundClientIps stores IP addresses associated with inbound clients for access control. type InboundClientIps struct { Id int `json:"id" gorm:"primaryKey;autoIncrement"` ClientEmail string `json:"clientEmail" form:"clientEmail" gorm:"unique"` Ips string `json:"ips" form:"ips"` } // HistoryOfSeeders tracks which database seeders have been executed to prevent re-running. type HistoryOfSeeders struct { Id int `json:"id" gorm:"primaryKey;autoIncrement"` SeederName string `json:"seederName"` } // GenXrayInboundConfig generates an Xray inbound configuration from the Inbound model. func (i *Inbound) GenXrayInboundConfig() *xray.InboundConfig { listen := i.Listen // Default to 0.0.0.0 (all interfaces) when listen is empty // This ensures proper dual-stack IPv4/IPv6 binding in systems where bindv6only=0 if listen == "" { listen = "0.0.0.0" } listen = fmt.Sprintf("\"%v\"", listen) return &xray.InboundConfig{ Listen: json_util.RawMessage(listen), Port: i.Port, Protocol: string(i.Protocol), Settings: json_util.RawMessage(i.Settings), StreamSettings: json_util.RawMessage(i.StreamSettings), Tag: i.Tag, Sniffing: json_util.RawMessage(i.Sniffing), } } // Setting stores key-value configuration settings for the 3x-ui panel. type Setting struct { Id int `json:"id" form:"id" gorm:"primaryKey;autoIncrement"` Key string `json:"key" form:"key"` Value string `json:"value" form:"value"` } // Client represents a client configuration for Xray inbounds with traffic limits and settings. type Client struct { ID string `json:"id"` // Unique client identifier Security string `json:"security"` // Security method (e.g., "auto", "aes-128-gcm") Password string `json:"password"` // Client password Flow string `json:"flow"` // Flow control (XTLS) Email string `json:"email"` // Client email identifier LimitIP int `json:"limitIp"` // IP limit for this client TotalGB int64 `json:"totalGB" form:"totalGB"` // Total traffic limit in GB ExpiryTime int64 `json:"expiryTime" form:"expiryTime"` // Expiration timestamp Enable bool `json:"enable" form:"enable"` // Whether the client is enabled TgID int64 `json:"tgId" form:"tgId"` // Telegram user ID for notifications SubID string `json:"subId" form:"subId"` // Subscription identifier Comment string `json:"comment" form:"comment"` // Client comment Reset int `json:"reset" form:"reset"` // Reset period in days CreatedAt int64 `json:"created_at,omitempty"` // Creation timestamp UpdatedAt int64 `json:"updated_at,omitempty"` // Last update timestamp } ================================================ FILE: docker-compose.yml ================================================ services: 3xui: build: context: . dockerfile: ./Dockerfile container_name: 3xui_app # hostname: yourhostname <- optional volumes: - $PWD/db/:/etc/x-ui/ - $PWD/cert/:/root/cert/ environment: XRAY_VMESS_AEAD_FORCED: "false" XUI_ENABLE_FAIL2BAN: "true" tty: true network_mode: host restart: unless-stopped ================================================ FILE: go.mod ================================================ module github.com/mhsanaei/3x-ui/v2 go 1.26.0 require ( github.com/gin-contrib/gzip v1.2.5 github.com/gin-contrib/sessions v1.0.4 github.com/gin-gonic/gin v1.12.0 github.com/go-ldap/ldap/v3 v3.4.12 github.com/goccy/go-json v0.10.5 github.com/google/uuid v1.6.0 github.com/gorilla/websocket v1.5.3 github.com/joho/godotenv v1.5.1 github.com/mymmrac/telego v1.7.0 github.com/nicksnyder/go-i18n/v2 v2.6.1 github.com/op/go-logging v0.0.0-20160315200505-970db520ece7 github.com/pelletier/go-toml/v2 v2.2.4 github.com/robfig/cron/v3 v3.0.1 github.com/shirou/gopsutil/v4 v4.26.2 github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e github.com/valyala/fasthttp v1.69.0 github.com/xlzd/gotp v0.1.0 github.com/xtls/xray-core v1.260206.0 go.uber.org/atomic v1.11.0 golang.org/x/crypto v0.48.0 golang.org/x/sys v0.41.0 golang.org/x/text v0.34.0 google.golang.org/grpc v1.79.1 gorm.io/driver/sqlite v1.6.0 gorm.io/gorm v1.31.1 ) require ( github.com/Azure/go-ntlmssp v0.1.0 // indirect github.com/andybalholm/brotli v1.2.0 // indirect github.com/apernet/quic-go v0.57.2-0.20260111184307-eec823306178 // indirect github.com/bytedance/gopkg v0.1.3 // indirect github.com/bytedance/sonic v1.15.0 // indirect github.com/bytedance/sonic/loader v0.5.0 // indirect github.com/cloudflare/circl v1.6.3 // indirect github.com/cloudwego/base64x v0.1.6 // indirect github.com/ebitengine/purego v0.10.0 // indirect github.com/gabriel-vasile/mimetype v1.4.13 // indirect github.com/gin-contrib/sse v1.1.0 // indirect github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 // indirect github.com/go-ole/go-ole v1.3.0 // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-playground/validator/v10 v10.30.1 // indirect github.com/goccy/go-yaml v1.19.2 // indirect github.com/google/btree v1.1.3 // indirect github.com/gorilla/context v1.1.2 // indirect github.com/gorilla/securecookie v1.1.2 // indirect github.com/gorilla/sessions v1.4.0 // indirect github.com/grbit/go-json v0.11.0 // indirect github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/now v1.1.5 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/juju/ratelimit v1.0.2 // indirect github.com/klauspost/compress v1.18.4 // indirect github.com/klauspost/cpuid/v2 v2.3.0 // indirect github.com/leodido/go-urn v1.4.0 // indirect github.com/lufia/plan9stats v0.0.0-20260216142805-b3301c5f2a88 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-sqlite3 v1.14.34 // indirect github.com/miekg/dns v1.1.72 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/pires/go-proxyproto v0.11.0 // indirect github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect github.com/quic-go/qpack v0.6.0 // indirect github.com/quic-go/quic-go v0.59.0 // indirect github.com/refraction-networking/utls v1.8.2 // indirect github.com/rogpeppe/go-internal v1.14.1 // indirect github.com/sagernet/sing v0.8.1 // indirect github.com/sagernet/sing-shadowsocks v0.2.9 // indirect github.com/tklauser/go-sysconf v0.3.16 // indirect github.com/tklauser/numcpus v0.11.0 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/ugorji/go/codec v1.3.1 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/fastjson v1.6.10 // indirect github.com/vishvananda/netlink v1.3.1 // indirect github.com/vishvananda/netns v0.0.5 // indirect github.com/xtls/reality v0.0.0-20251116175510-cd53f7d50237 // indirect github.com/yusufpapurcu/wmi v1.2.4 // indirect go.mongodb.org/mongo-driver/v2 v2.5.0 // indirect go4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect golang.org/x/arch v0.24.0 // indirect golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa // indirect golang.org/x/mod v0.33.0 // indirect golang.org/x/net v0.51.0 // indirect golang.org/x/sync v0.19.0 // indirect golang.org/x/time v0.14.0 // indirect golang.org/x/tools v0.42.0 // indirect golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 // indirect google.golang.org/protobuf v1.36.11 // indirect gvisor.dev/gvisor v0.0.0-20260122175437-89a5d21be8f0 // indirect lukechampine.com/blake3 v1.4.1 // indirect ) ================================================ FILE: go.sum ================================================ github.com/Azure/go-ntlmssp v0.1.0 h1:DjFo6YtWzNqNvQdrwEyr/e4nhU3vRiwenz5QX7sFz+A= github.com/Azure/go-ntlmssp v0.1.0/go.mod h1:NYqdhxd/8aAct/s4qSYZEerdPuH1liG2/X9DiVTbhpk= github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk= github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/alexbrainman/sspi v0.0.0-20250919150558-7d374ff0d59e h1:4dAU9FXIyQktpoUAgOJK3OTFc/xug0PCXYCqU0FgDKI= github.com/alexbrainman/sspi v0.0.0-20250919150558-7d374ff0d59e/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4= github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ= github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= github.com/apernet/quic-go v0.57.2-0.20260111184307-eec823306178 h1:bSq8n+gX4oO/qnM3MKf4kroW75n+phO9Qp6nigJKZ1E= github.com/apernet/quic-go v0.57.2-0.20260111184307-eec823306178/go.mod h1:N1WIjPphkqs4efXWuyDNQ6OjjIK04vM3h+bEgwV+eVU= github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M= github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM= github.com/bytedance/sonic v1.15.0 h1:/PXeWFaR5ElNcVE84U0dOHjiMHQOwNIx3K4ymzh/uSE= github.com/bytedance/sonic v1.15.0/go.mod h1:tFkWrPz0/CUCLEF4ri4UkHekCIcdnkqXw9VduqpJh0k= github.com/bytedance/sonic/loader v0.5.0 h1:gXH3KVnatgY7loH5/TkeVyXPfESoqSBSBEiDd5VjlgE= github.com/bytedance/sonic/loader v0.5.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8= github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4= github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M= github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/ebitengine/purego v0.10.0 h1:QIw4xfpWT6GWTzaW5XEKy3HXoqrJGx1ijYHzTF0/ISU= github.com/ebitengine/purego v0.10.0/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= github.com/gabriel-vasile/mimetype v1.4.13 h1:46nXokslUBsAJE/wMsp5gtO500a4F3Nkz9Ufpk2AcUM= github.com/gabriel-vasile/mimetype v1.4.13/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= github.com/ghodss/yaml v1.0.1-0.20220118164431-d8423dcdf344 h1:Arcl6UOIS/kgO2nW3A65HN+7CMjSDP/gofXL4CZt1V4= github.com/ghodss/yaml v1.0.1-0.20220118164431-d8423dcdf344/go.mod h1:GIjDIg/heH5DOkXY3YJ/wNhfHsQHoXGjl8G8amsYQ1I= github.com/gin-contrib/gzip v1.2.5 h1:fIZs0S+l17pIu1P5XRJOo/YNqfIuPCrZZ3TWB7pjckI= github.com/gin-contrib/gzip v1.2.5/go.mod h1:aomRgR7ftdZV3uWY0gW/m8rChfxau0n8YVvwlOHONzw= github.com/gin-contrib/sessions v1.0.4 h1:ha6CNdpYiTOK/hTp05miJLbpTSNfOnFg5Jm2kbcqy8U= github.com/gin-contrib/sessions v1.0.4/go.mod h1:ccmkrb2z6iU2osiAHZG3x3J4suJK+OU27oqzlWOqQgs= github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w= github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM= github.com/gin-gonic/gin v1.12.0 h1:b3YAbrZtnf8N//yjKeU2+MQsh2mY5htkZidOM7O0wG8= github.com/gin-gonic/gin v1.12.0/go.mod h1:VxccKfsSllpKshkBWgVgRniFFAzFb9csfngsqANjnLc= github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 h1:BP4M0CvQ4S3TGls2FvczZtj5Re/2ZzkV9VwqPHH/3Bo= github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0= github.com/go-ldap/ldap/v3 v3.4.12 h1:1b81mv7MagXZ7+1r7cLTWmyuTqVqdwbtJSjC0DAp9s4= github.com/go-ldap/ldap/v3 v3.4.12/go.mod h1:+SPAGcTtOfmGsCb3h1RFiq4xpp4N636G75OEace8lNo= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= github.com/go-playground/validator/v10 v10.30.1 h1:f3zDSN/zOma+w6+1Wswgd9fLkdwy06ntQJp0BBvFG0w= github.com/go-playground/validator/v10 v10.30.1/go.mod h1:oSuBIQzuJxL//3MelwSLD5hc2Tu889bF0Idm9Dg26cM= github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM= github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= github.com/golang/mock v1.7.0-rc.1 h1:YojYx61/OLFsiv6Rw1Z96LpldJIy31o+UHmwAUMJ6/U= github.com/golang/mock v1.7.0-rc.1/go.mod h1:s42URUywIqd+OcERslBJvOjepvNymP31m3q8d/GkuRs= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg= github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/context v1.1.2 h1:WRkNAv2uoa03QNIc1A6u4O7DAGMUVoopZhkiXWA2V1o= github.com/gorilla/context v1.1.2/go.mod h1:KDPwT9i/MeWHiLl90fuTgrt4/wPcv75vFAZLaOOcbxM= github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA= github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo= github.com/gorilla/sessions v1.4.0 h1:kpIYOp/oi6MG/p5PgxApU8srsSw9tuFbt46Lt7auzqQ= github.com/gorilla/sessions v1.4.0/go.mod h1:FLWm50oby91+hl7p/wRxDth9bWSuk0qVL2emc7lT5ik= github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/grbit/go-json v0.11.0 h1:bAbyMdYrYl/OjYsSqLH99N2DyQ291mHy726Mx+sYrnc= github.com/grbit/go-json v0.11.0/go.mod h1:IYpHsdybQ386+6g3VE6AXQ3uTGa5mquBme5/ZWmtzek= github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/jcmturner/aescts/v2 v2.0.0 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFKrle8= github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs= github.com/jcmturner/dnsutils/v2 v2.0.0 h1:lltnkeZGL0wILNvrNiVCR6Ro5PGU/SeBvVO/8c/iPbo= github.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM= github.com/jcmturner/gofork v1.7.6 h1:QH0l3hzAU1tfT3rZCnW5zXl+orbkNMMRGJfdJjHVETg= github.com/jcmturner/gofork v1.7.6/go.mod h1:1622LH6i/EZqLloHfE7IeZ0uEJwMSUyQ/nDd82IeqRo= github.com/jcmturner/goidentity/v6 v6.0.1 h1:VKnZd2oEIMorCTsFBnJWbExfNN7yZr3EhJAxwOkZg6o= github.com/jcmturner/goidentity/v6 v6.0.1/go.mod h1:X1YW3bgtvwAXju7V3LCIMpY0Gbxyjn/mY9zx4tFonSg= github.com/jcmturner/gokrb5/v8 v8.4.4 h1:x1Sv4HaTpepFkXbt2IkL29DXRf8sOfZXo8eRKh687T8= github.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs= github.com/jcmturner/rpc/v2 v2.0.3 h1:7FXXj8Ti1IaVFpSAziCZWNzbNuZmnvw/i6CqLNdWfZY= github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc= github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/juju/ratelimit v1.0.2 h1:sRxmtRiajbvrcLQT7S+JbqU0ntsb9W2yhSdNN8tWfaI= github.com/juju/ratelimit v1.0.2/go.mod h1:qapgC/Gy+xNh9UxzV13HGGl/6UXNN+ct+vwSgWNm/qk= github.com/klauspost/compress v1.18.4 h1:RPhnKRAQ4Fh8zU2FY/6ZFDwTVTxgJ/EMydqSTzE9a2c= github.com/klauspost/compress v1.18.4/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= github.com/lufia/plan9stats v0.0.0-20260216142805-b3301c5f2a88 h1:PTw+yKnXcOFCR6+8hHTyWBeQ/P4Nb7dd4/0ohEcWQuM= github.com/lufia/plan9stats v0.0.0-20260216142805-b3301c5f2a88/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-sqlite3 v1.14.34 h1:3NtcvcUnFBPsuRcno8pUtupspG/GM+9nZ88zgJcp6Zk= github.com/mattn/go-sqlite3 v1.14.34/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/miekg/dns v1.1.72 h1:vhmr+TF2A3tuoGNkLDFK9zi36F2LS+hKTRW0Uf8kbzI= github.com/miekg/dns v1.1.72/go.mod h1:+EuEPhdHOsfk6Wk5TT2CzssZdqkmFhf8r+aVyDEToIs= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/mymmrac/telego v1.7.0 h1:yRO/l00tFGG4nY66ufUKb4ARqv7qx9+LsjQv/b0NEyo= github.com/mymmrac/telego v1.7.0/go.mod h1:pdLV346EgVuq7Xrh3kMggeBiazeHhsdEoK0RTEOPXRM= github.com/nicksnyder/go-i18n/v2 v2.6.1 h1:JDEJraFsQE17Dut9HFDHzCoAWGEQJom5s0TRd17NIEQ= github.com/nicksnyder/go-i18n/v2 v2.6.1/go.mod h1:Vee0/9RD3Quc/NmwEjzzD7VTZ+Ir7QbXocrkhOzmUKA= github.com/op/go-logging v0.0.0-20160315200505-970db520ece7 h1:lDH9UUVJtmYCjyT0CI4q8xvlXPxeZ0gYCVvWbmPlp88= github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk= github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= github.com/pires/go-proxyproto v0.11.0 h1:gUQpS85X/VJMdUsYyEgyn59uLJvGqPhJV5YvG68wXH4= github.com/pires/go-proxyproto v0.11.0/go.mod h1:ZKAAyp3cgy5Y5Mo4n9AlScrkCZwUy0g3Jf+slqQVcuU= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU= github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8= github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII= github.com/quic-go/quic-go v0.59.0 h1:OLJkp1Mlm/aS7dpKgTc6cnpynnD2Xg7C1pwL6vy/SAw= github.com/quic-go/quic-go v0.59.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU= github.com/refraction-networking/utls v1.8.2 h1:j4Q1gJj0xngdeH+Ox/qND11aEfhpgoEvV+S9iJ2IdQo= github.com/refraction-networking/utls v1.8.2/go.mod h1:jkSOEkLqn+S/jtpEHPOsVv/4V4EVnelwbMQl4vCWXAM= github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/sagernet/sing v0.8.1 h1:Li+zg4xdiMsvdX4j50TPqmSG8LF/TB9US2qlAN40izU= github.com/sagernet/sing v0.8.1/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak= github.com/sagernet/sing-shadowsocks v0.2.9 h1:Paep5zCszRKsEn8587O0MnhFWKJwDW1Y4zOYYlIxMkM= github.com/sagernet/sing-shadowsocks v0.2.9/go.mod h1:TE/Z6401Pi8tgr0nBZcM/xawAI6u3F6TTbz4nH/qw+8= github.com/shirou/gopsutil/v4 v4.26.2 h1:X8i6sicvUFih4BmYIGT1m2wwgw2VG9YgrDTi7cIRGUI= github.com/shirou/gopsutil/v4 v4.26.2/go.mod h1:LZ6ewCSkBqUpvSOf+LsTGnRinC6iaNUNMGBtDkJBaLQ= github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0= github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/tklauser/go-sysconf v0.3.16 h1:frioLaCQSsF5Cy1jgRBrzr6t502KIIwQ0MArYICU0nA= github.com/tklauser/go-sysconf v0.3.16/go.mod h1:/qNL9xxDhc7tx3HSRsLWNnuzbVfh3e7gh/BmM179nYI= github.com/tklauser/numcpus v0.11.0 h1:nSTwhKH5e1dMNsCdVBukSZrURJRoHbSEQjdEbY+9RXw= github.com/tklauser/numcpus v0.11.0/go.mod h1:z+LwcLq54uWZTX0u/bGobaV34u6V7KNlTZejzM6/3MQ= github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= github.com/ugorji/go/codec v1.3.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY= github.com/ugorji/go/codec v1.3.1/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/fasthttp v1.69.0 h1:fNLLESD2SooWeh2cidsuFtOcrEi4uB4m1mPrkJMZyVI= github.com/valyala/fasthttp v1.69.0/go.mod h1:4wA4PfAraPlAsJ5jMSqCE2ug5tqUPwKXxVj8oNECGcw= github.com/valyala/fastjson v1.6.10 h1:/yjJg8jaVQdYR3arGxPE2X5z89xrlhS0eGXdv+ADTh4= github.com/valyala/fastjson v1.6.10/go.mod h1:e6FubmQouUNP73jtMLmcbxS6ydWIpOfhz34TSfO3JaE= github.com/vishvananda/netlink v1.3.1 h1:3AEMt62VKqz90r0tmNhog0r/PpWKmrEShJU0wJW6bV0= github.com/vishvananda/netlink v1.3.1/go.mod h1:ARtKouGSTGchR8aMwmkzC0qiNPrrWO5JS/XMVl45+b4= github.com/vishvananda/netns v0.0.5 h1:DfiHV+j8bA32MFM7bfEunvT8IAqQ/NzSJHtcmW5zdEY= github.com/vishvananda/netns v0.0.5/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM= github.com/xlzd/gotp v0.1.0 h1:37blvlKCh38s+fkem+fFh7sMnceltoIEBYTVXyoa5Po= github.com/xlzd/gotp v0.1.0/go.mod h1:ndLJ3JKzi3xLmUProq4LLxCuECL93dG9WASNLpHz8qg= github.com/xtls/reality v0.0.0-20251116175510-cd53f7d50237 h1:UXjrmniKlY+ZbIqpN91lejB3pszQQQRVu1vqH/p/aGM= github.com/xtls/reality v0.0.0-20251116175510-cd53f7d50237/go.mod h1:vbHCV/3VWUvy1oKvTxxWJRPEWSeR1sYgQHIh6u/JiZQ= github.com/xtls/xray-core v1.260206.0 h1:gY8IV6u76CW93txL9QmacgZ0Udxr2Q3e9qUxXAhdHqI= github.com/xtls/xray-core v1.260206.0/go.mod h1:GyFIgVGRJkt3eyV/NMcdxOKXcJPqGGpyupHzy16uJhU= github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= go.mongodb.org/mongo-driver/v2 v2.5.0 h1:yXUhImUjjAInNcpTcAlPHiT7bIXhshCTL3jVBkF3xaE= go.mongodb.org/mongo-driver/v2 v2.5.0/go.mod h1:yOI9kBsufol30iFsl1slpdq1I0eHPzybRWdyYUs8K/0= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48= go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8= go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0= go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs= go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18= go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE= go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8= go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew= go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI= go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA= go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y= go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBseWJUpBw5I82+2U4M= go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y= golang.org/x/arch v0.24.0 h1:qlJ3M9upxvFfwRM51tTg3Yl+8CP9vCC1E7vlFpgv99Y= golang.org/x/arch v0.24.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A= golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa h1:Zt3DZoOFFYkKhDT3v7Lm9FDMEV06GpzjG2jrqW+QTE0= golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa/go.mod h1:K79w1Vqn7PoiZn+TkNpx3BUWUQksGO3JcVX6qIjytmA= golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8= golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w= golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo= golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y= golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k= golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0= golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeunTOisW56dUokqW/FOteYJJ/yg= golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI= golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb h1:whnFRlWMcXI9d+ZbWg+4sHnLp52d5yiIPUxMBSt4X9A= golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb/go.mod h1:rpwXGsirqLqN2L0JDJQlwOboGHmptD5ZD6T2VmcqhTw= gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 h1:ggcbiqK8WWh6l1dnltU4BgWGIGo+EVYxCaAPih/zQXQ= google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= google.golang.org/grpc v1.79.1 h1:zGhSi45ODB9/p3VAawt9a+O/MULLl9dpizzNNpq7flY= google.golang.org/grpc v1.79.1/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gorm.io/driver/sqlite v1.6.0 h1:WHRRrIiulaPiPFmDcod6prc4l2VGVWHz80KspNsxSfQ= gorm.io/driver/sqlite v1.6.0/go.mod h1:AO9V1qIQddBESngQUKWL9yoH93HIeA1X6V633rBwyT8= gorm.io/gorm v1.31.1 h1:7CA8FTFz/gRfgqgpeKIBcervUn3xSyPUmr6B2WXJ7kg= gorm.io/gorm v1.31.1/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs= gvisor.dev/gvisor v0.0.0-20260122175437-89a5d21be8f0 h1:Lk6hARj5UPY47dBep70OD/TIMwikJ5fGUGX0Rm3Xigk= gvisor.dev/gvisor v0.0.0-20260122175437-89a5d21be8f0/go.mod h1:QkHjoMIBaYtpVufgwv3keYAbln78mBoCuShZrPrer1Q= lukechampine.com/blake3 v1.4.1 h1:I3Smz7gso8w4/TunLKec6K2fn+kyKtDxr/xcQEN84Wg= lukechampine.com/blake3 v1.4.1/go.mod h1:QFosUxmjB8mnrWFSNwKmvxHpfY72bmD2tQ0kBMM3kwo= ================================================ FILE: install.sh ================================================ #!/bin/bash red='\033[0;31m' green='\033[0;32m' blue='\033[0;34m' yellow='\033[0;33m' plain='\033[0m' cur_dir=$(pwd) xui_folder="${XUI_MAIN_FOLDER:=/usr/local/x-ui}" xui_service="${XUI_SERVICE:=/etc/systemd/system}" # check root [[ $EUID -ne 0 ]] && echo -e "${red}Fatal error: ${plain} Please run this script with root privilege \n " && exit 1 # Check OS and set release variable if [[ -f /etc/os-release ]]; then source /etc/os-release release=$ID elif [[ -f /usr/lib/os-release ]]; then source /usr/lib/os-release release=$ID else echo "Failed to check the system OS, please contact the author!" >&2 exit 1 fi echo "The OS release is: $release" arch() { case "$(uname -m)" in x86_64 | x64 | amd64) echo 'amd64' ;; i*86 | x86) echo '386' ;; armv8* | armv8 | arm64 | aarch64) echo 'arm64' ;; armv7* | armv7 | arm) echo 'armv7' ;; armv6* | armv6) echo 'armv6' ;; armv5* | armv5) echo 'armv5' ;; s390x) echo 's390x' ;; *) echo -e "${green}Unsupported CPU architecture! ${plain}" && rm -f install.sh && exit 1 ;; esac } echo "Arch: $(arch)" # Simple helpers is_ipv4() { [[ "$1" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]] && return 0 || return 1 } is_ipv6() { [[ "$1" =~ : ]] && return 0 || return 1 } is_ip() { is_ipv4 "$1" || is_ipv6 "$1" } is_domain() { [[ "$1" =~ ^([A-Za-z0-9](-*[A-Za-z0-9])*\.)+(xn--[a-z0-9]{2,}|[A-Za-z]{2,})$ ]] && return 0 || return 1 } # Port helpers is_port_in_use() { local port="$1" if command -v ss >/dev/null 2>&1; then ss -ltn 2>/dev/null | awk -v p=":${port}$" '$4 ~ p {exit 0} END {exit 1}' return fi if command -v netstat >/dev/null 2>&1; then netstat -lnt 2>/dev/null | awk -v p=":${port} " '$4 ~ p {exit 0} END {exit 1}' return fi if command -v lsof >/dev/null 2>&1; then lsof -nP -iTCP:${port} -sTCP:LISTEN >/dev/null 2>&1 && return 0 fi return 1 } install_base() { case "${release}" in ubuntu | debian | armbian) apt-get update && apt-get install -y -q cron curl tar tzdata socat ca-certificates ;; fedora | amzn | virtuozzo | rhel | almalinux | rocky | ol) dnf -y update && dnf install -y -q curl tar tzdata socat ca-certificates ;; centos) if [[ "${VERSION_ID}" =~ ^7 ]]; then yum -y update && yum install -y curl tar tzdata socat ca-certificates else dnf -y update && dnf install -y -q curl tar tzdata socat ca-certificates fi ;; arch | manjaro | parch) pacman -Syu && pacman -Syu --noconfirm curl tar tzdata socat ca-certificates ;; opensuse-tumbleweed | opensuse-leap) zypper refresh && zypper -q install -y curl tar timezone socat ca-certificates ;; alpine) apk update && apk add curl tar tzdata socat ca-certificates ;; *) apt-get update && apt-get install -y -q curl tar tzdata socat ca-certificates ;; esac } gen_random_string() { local length="$1" local random_string=$(LC_ALL=C tr -dc 'a-zA-Z0-9' /dev/null 2>&1 if [ $? -ne 0 ]; then echo -e "${red}Failed to install acme.sh${plain}" return 1 else echo -e "${green}acme.sh installed successfully${plain}" fi return 0 } setup_ssl_certificate() { local domain="$1" local server_ip="$2" local existing_port="$3" local existing_webBasePath="$4" echo -e "${green}Setting up SSL certificate...${plain}" # Check if acme.sh is installed if ! command -v ~/.acme.sh/acme.sh &>/dev/null; then install_acme if [ $? -ne 0 ]; then echo -e "${yellow}Failed to install acme.sh, skipping SSL setup${plain}" return 1 fi fi # Create certificate directory local certPath="/root/cert/${domain}" mkdir -p "$certPath" # Issue certificate echo -e "${green}Issuing SSL certificate for ${domain}...${plain}" echo -e "${yellow}Note: Port 80 must be open and accessible from the internet${plain}" ~/.acme.sh/acme.sh --set-default-ca --server letsencrypt --force >/dev/null 2>&1 ~/.acme.sh/acme.sh --issue -d ${domain} --listen-v6 --standalone --httpport 80 --force if [ $? -ne 0 ]; then echo -e "${yellow}Failed to issue certificate for ${domain}${plain}" echo -e "${yellow}Please ensure port 80 is open and try again later with: x-ui${plain}" rm -rf ~/.acme.sh/${domain} 2>/dev/null rm -rf "$certPath" 2>/dev/null return 1 fi # Install certificate ~/.acme.sh/acme.sh --installcert -d ${domain} \ --key-file /root/cert/${domain}/privkey.pem \ --fullchain-file /root/cert/${domain}/fullchain.pem \ --reloadcmd "systemctl restart x-ui" >/dev/null 2>&1 if [ $? -ne 0 ]; then echo -e "${yellow}Failed to install certificate${plain}" return 1 fi # Enable auto-renew ~/.acme.sh/acme.sh --upgrade --auto-upgrade >/dev/null 2>&1 # Secure permissions: private key readable only by owner chmod 600 $certPath/privkey.pem 2>/dev/null chmod 644 $certPath/fullchain.pem 2>/dev/null # Set certificate for panel local webCertFile="/root/cert/${domain}/fullchain.pem" local webKeyFile="/root/cert/${domain}/privkey.pem" if [[ -f "$webCertFile" && -f "$webKeyFile" ]]; then ${xui_folder}/x-ui cert -webCert "$webCertFile" -webCertKey "$webKeyFile" >/dev/null 2>&1 echo -e "${green}SSL certificate installed and configured successfully!${plain}" return 0 else echo -e "${yellow}Certificate files not found${plain}" return 1 fi } # Issue Let's Encrypt IP certificate with shortlived profile (~6 days validity) # Requires acme.sh and port 80 open for HTTP-01 challenge setup_ip_certificate() { local ipv4="$1" local ipv6="$2" # optional echo -e "${green}Setting up Let's Encrypt IP certificate (shortlived profile)...${plain}" echo -e "${yellow}Note: IP certificates are valid for ~6 days and will auto-renew.${plain}" echo -e "${yellow}Default listener is port 80. If you choose another port, ensure external port 80 forwards to it.${plain}" # Check for acme.sh if ! command -v ~/.acme.sh/acme.sh &>/dev/null; then install_acme if [ $? -ne 0 ]; then echo -e "${red}Failed to install acme.sh${plain}" return 1 fi fi # Validate IP address if [[ -z "$ipv4" ]]; then echo -e "${red}IPv4 address is required${plain}" return 1 fi if ! is_ipv4 "$ipv4"; then echo -e "${red}Invalid IPv4 address: $ipv4${plain}" return 1 fi # Create certificate directory local certDir="/root/cert/ip" mkdir -p "$certDir" # Build domain arguments local domain_args="-d ${ipv4}" if [[ -n "$ipv6" ]] && is_ipv6 "$ipv6"; then domain_args="${domain_args} -d ${ipv6}" echo -e "${green}Including IPv6 address: ${ipv6}${plain}" fi # Set reload command for auto-renewal (add || true so it doesn't fail during first install) local reloadCmd="systemctl restart x-ui 2>/dev/null || rc-service x-ui restart 2>/dev/null || true" # Choose port for HTTP-01 listener (default 80, prompt override) local WebPort="" read -rp "Port to use for ACME HTTP-01 listener (default 80): " WebPort WebPort="${WebPort:-80}" if ! [[ "${WebPort}" =~ ^[0-9]+$ ]] || ((WebPort < 1 || WebPort > 65535)); then echo -e "${red}Invalid port provided. Falling back to 80.${plain}" WebPort=80 fi echo -e "${green}Using port ${WebPort} for standalone validation.${plain}" if [[ "${WebPort}" -ne 80 ]]; then echo -e "${yellow}Reminder: Let's Encrypt still connects on port 80; forward external port 80 to ${WebPort}.${plain}" fi # Ensure chosen port is available while true; do if is_port_in_use "${WebPort}"; then echo -e "${yellow}Port ${WebPort} is in use.${plain}" local alt_port="" read -rp "Enter another port for acme.sh standalone listener (leave empty to abort): " alt_port alt_port="${alt_port// /}" if [[ -z "${alt_port}" ]]; then echo -e "${red}Port ${WebPort} is busy; cannot proceed.${plain}" return 1 fi if ! [[ "${alt_port}" =~ ^[0-9]+$ ]] || ((alt_port < 1 || alt_port > 65535)); then echo -e "${red}Invalid port provided.${plain}" return 1 fi WebPort="${alt_port}" continue else echo -e "${green}Port ${WebPort} is free and ready for standalone validation.${plain}" break fi done # Issue certificate with shortlived profile echo -e "${green}Issuing IP certificate for ${ipv4}...${plain}" ~/.acme.sh/acme.sh --set-default-ca --server letsencrypt --force >/dev/null 2>&1 ~/.acme.sh/acme.sh --issue \ ${domain_args} \ --standalone \ --server letsencrypt \ --certificate-profile shortlived \ --days 6 \ --httpport ${WebPort} \ --force if [ $? -ne 0 ]; then echo -e "${red}Failed to issue IP certificate${plain}" echo -e "${yellow}Please ensure port ${WebPort} is reachable (or forwarded from external port 80)${plain}" # Cleanup acme.sh data for both IPv4 and IPv6 if specified rm -rf ~/.acme.sh/${ipv4} 2>/dev/null [[ -n "$ipv6" ]] && rm -rf ~/.acme.sh/${ipv6} 2>/dev/null rm -rf ${certDir} 2>/dev/null return 1 fi echo -e "${green}Certificate issued successfully, installing...${plain}" # Install certificate # Note: acme.sh may report "Reload error" and exit non-zero if reloadcmd fails, # but the cert files are still installed. We check for files instead of exit code. ~/.acme.sh/acme.sh --installcert -d ${ipv4} \ --key-file "${certDir}/privkey.pem" \ --fullchain-file "${certDir}/fullchain.pem" \ --reloadcmd "${reloadCmd}" 2>&1 || true # Verify certificate files exist (don't rely on exit code - reloadcmd failure causes non-zero) if [[ ! -f "${certDir}/fullchain.pem" || ! -f "${certDir}/privkey.pem" ]]; then echo -e "${red}Certificate files not found after installation${plain}" # Cleanup acme.sh data for both IPv4 and IPv6 if specified rm -rf ~/.acme.sh/${ipv4} 2>/dev/null [[ -n "$ipv6" ]] && rm -rf ~/.acme.sh/${ipv6} 2>/dev/null rm -rf ${certDir} 2>/dev/null return 1 fi echo -e "${green}Certificate files installed successfully${plain}" # Enable auto-upgrade for acme.sh (ensures cron job runs) ~/.acme.sh/acme.sh --upgrade --auto-upgrade >/dev/null 2>&1 # Secure permissions: private key readable only by owner chmod 600 ${certDir}/privkey.pem 2>/dev/null chmod 644 ${certDir}/fullchain.pem 2>/dev/null # Configure panel to use the certificate echo -e "${green}Setting certificate paths for the panel...${plain}" ${xui_folder}/x-ui cert -webCert "${certDir}/fullchain.pem" -webCertKey "${certDir}/privkey.pem" if [ $? -ne 0 ]; then echo -e "${yellow}Warning: Could not set certificate paths automatically${plain}" echo -e "${yellow}Certificate files are at:${plain}" echo -e " Cert: ${certDir}/fullchain.pem" echo -e " Key: ${certDir}/privkey.pem" else echo -e "${green}Certificate paths configured successfully${plain}" fi echo -e "${green}IP certificate installed and configured successfully!${plain}" echo -e "${green}Certificate valid for ~6 days, auto-renews via acme.sh cron job.${plain}" echo -e "${yellow}acme.sh will automatically renew and reload x-ui before expiry.${plain}" return 0 } # Comprehensive manual SSL certificate issuance via acme.sh ssl_cert_issue() { local existing_webBasePath=$(${xui_folder}/x-ui setting -show true | grep 'webBasePath:' | awk -F': ' '{print $2}' | tr -d '[:space:]' | sed 's#^/##') local existing_port=$(${xui_folder}/x-ui setting -show true | grep 'port:' | awk -F': ' '{print $2}' | tr -d '[:space:]') # check for acme.sh first if ! command -v ~/.acme.sh/acme.sh &>/dev/null; then echo "acme.sh could not be found. Installing now..." cd ~ || return 1 curl -s https://get.acme.sh | sh if [ $? -ne 0 ]; then echo -e "${red}Failed to install acme.sh${plain}" return 1 else echo -e "${green}acme.sh installed successfully${plain}" fi fi # get the domain here, and we need to verify it local domain="" while true; do read -rp "Please enter your domain name: " domain domain="${domain// /}" # Trim whitespace if [[ -z "$domain" ]]; then echo -e "${red}Domain name cannot be empty. Please try again.${plain}" continue fi if ! is_domain "$domain"; then echo -e "${red}Invalid domain format: ${domain}. Please enter a valid domain name.${plain}" continue fi break done echo -e "${green}Your domain is: ${domain}, checking it...${plain}" # check if there already exists a certificate local currentCert=$(~/.acme.sh/acme.sh --list | tail -1 | awk '{print $1}') if [ "${currentCert}" == "${domain}" ]; then local certInfo=$(~/.acme.sh/acme.sh --list) echo -e "${red}System already has certificates for this domain. Cannot issue again.${plain}" echo -e "${yellow}Current certificate details:${plain}" echo "$certInfo" return 1 else echo -e "${green}Your domain is ready for issuing certificates now...${plain}" fi # create a directory for the certificate certPath="/root/cert/${domain}" if [ ! -d "$certPath" ]; then mkdir -p "$certPath" else rm -rf "$certPath" mkdir -p "$certPath" fi # get the port number for the standalone server local WebPort=80 read -rp "Please choose which port to use (default is 80): " WebPort if [[ ${WebPort} -gt 65535 || ${WebPort} -lt 1 ]]; then echo -e "${yellow}Your input ${WebPort} is invalid, will use default port 80.${plain}" WebPort=80 fi echo -e "${green}Will use port: ${WebPort} to issue certificates. Please make sure this port is open.${plain}" # Stop panel temporarily echo -e "${yellow}Stopping panel temporarily...${plain}" systemctl stop x-ui 2>/dev/null || rc-service x-ui stop 2>/dev/null # issue the certificate ~/.acme.sh/acme.sh --set-default-ca --server letsencrypt --force ~/.acme.sh/acme.sh --issue -d ${domain} --listen-v6 --standalone --httpport ${WebPort} --force if [ $? -ne 0 ]; then echo -e "${red}Issuing certificate failed, please check logs.${plain}" rm -rf ~/.acme.sh/${domain} systemctl start x-ui 2>/dev/null || rc-service x-ui start 2>/dev/null return 1 else echo -e "${green}Issuing certificate succeeded, installing certificates...${plain}" fi # Setup reload command reloadCmd="systemctl restart x-ui || rc-service x-ui restart" echo -e "${green}Default --reloadcmd for ACME is: ${yellow}systemctl restart x-ui || rc-service x-ui restart${plain}" echo -e "${green}This command will run on every certificate issue and renew.${plain}" read -rp "Would you like to modify --reloadcmd for ACME? (y/n): " setReloadcmd if [[ "$setReloadcmd" == "y" || "$setReloadcmd" == "Y" ]]; then echo -e "\n${green}\t1.${plain} Preset: systemctl reload nginx ; systemctl restart x-ui" echo -e "${green}\t2.${plain} Input your own command" echo -e "${green}\t0.${plain} Keep default reloadcmd" read -rp "Choose an option: " choice case "$choice" in 1) echo -e "${green}Reloadcmd is: systemctl reload nginx ; systemctl restart x-ui${plain}" reloadCmd="systemctl reload nginx ; systemctl restart x-ui" ;; 2) echo -e "${yellow}It's recommended to put x-ui restart at the end${plain}" read -rp "Please enter your custom reloadcmd: " reloadCmd echo -e "${green}Reloadcmd is: ${reloadCmd}${plain}" ;; *) echo -e "${green}Keeping default reloadcmd${plain}" ;; esac fi # install the certificate ~/.acme.sh/acme.sh --installcert -d ${domain} \ --key-file /root/cert/${domain}/privkey.pem \ --fullchain-file /root/cert/${domain}/fullchain.pem --reloadcmd "${reloadCmd}" if [ $? -ne 0 ]; then echo -e "${red}Installing certificate failed, exiting.${plain}" rm -rf ~/.acme.sh/${domain} systemctl start x-ui 2>/dev/null || rc-service x-ui start 2>/dev/null return 1 else echo -e "${green}Installing certificate succeeded, enabling auto renew...${plain}" fi # enable auto-renew ~/.acme.sh/acme.sh --upgrade --auto-upgrade if [ $? -ne 0 ]; then echo -e "${yellow}Auto renew setup had issues, certificate details:${plain}" ls -lah /root/cert/${domain}/ # Secure permissions: private key readable only by owner chmod 600 $certPath/privkey.pem 2>/dev/null chmod 644 $certPath/fullchain.pem 2>/dev/null else echo -e "${green}Auto renew succeeded, certificate details:${plain}" ls -lah /root/cert/${domain}/ # Secure permissions: private key readable only by owner chmod 600 $certPath/privkey.pem 2>/dev/null chmod 644 $certPath/fullchain.pem 2>/dev/null fi # start panel systemctl start x-ui 2>/dev/null || rc-service x-ui start 2>/dev/null # Prompt user to set panel paths after successful certificate installation read -rp "Would you like to set this certificate for the panel? (y/n): " setPanel if [[ "$setPanel" == "y" || "$setPanel" == "Y" ]]; then local webCertFile="/root/cert/${domain}/fullchain.pem" local webKeyFile="/root/cert/${domain}/privkey.pem" if [[ -f "$webCertFile" && -f "$webKeyFile" ]]; then ${xui_folder}/x-ui cert -webCert "$webCertFile" -webCertKey "$webKeyFile" echo -e "${green}Certificate paths set for the panel${plain}" echo -e "${green}Certificate File: $webCertFile${plain}" echo -e "${green}Private Key File: $webKeyFile${plain}" echo "" echo -e "${green}Access URL: https://${domain}:${existing_port}/${existing_webBasePath}${plain}" echo -e "${yellow}Panel will restart to apply SSL certificate...${plain}" systemctl restart x-ui 2>/dev/null || rc-service x-ui restart 2>/dev/null else echo -e "${red}Error: Certificate or private key file not found for domain: $domain.${plain}" fi else echo -e "${yellow}Skipping panel path setting.${plain}" fi return 0 } # Reusable interactive SSL setup (domain or IP) # Sets global `SSL_HOST` to the chosen domain/IP for Access URL usage prompt_and_setup_ssl() { local panel_port="$1" local web_base_path="$2" # expected without leading slash local server_ip="$3" local ssl_choice="" echo -e "${yellow}Choose SSL certificate setup method:${plain}" echo -e "${green}1.${plain} Let's Encrypt for Domain (90-day validity, auto-renews)" echo -e "${green}2.${plain} Let's Encrypt for IP Address (6-day validity, auto-renews)" echo -e "${green}3.${plain} Custom SSL Certificate (Path to existing files)" echo -e "${blue}Note:${plain} Options 1 & 2 require port 80 open. Option 3 requires manual paths." read -rp "Choose an option (default 2 for IP): " ssl_choice ssl_choice="${ssl_choice// /}" # Trim whitespace # Default to 2 (IP cert) if input is empty or invalid (not 1 or 3) if [[ "$ssl_choice" != "1" && "$ssl_choice" != "3" ]]; then ssl_choice="2" fi case "$ssl_choice" in 1) # User chose Let's Encrypt domain option echo -e "${green}Using Let's Encrypt for domain certificate...${plain}" ssl_cert_issue # Extract the domain that was used from the certificate local cert_domain=$(~/.acme.sh/acme.sh --list 2>/dev/null | tail -1 | awk '{print $1}') if [[ -n "${cert_domain}" ]]; then SSL_HOST="${cert_domain}" echo -e "${green}✓ SSL certificate configured successfully with domain: ${cert_domain}${plain}" else echo -e "${yellow}SSL setup may have completed, but domain extraction failed${plain}" SSL_HOST="${server_ip}" fi ;; 2) # User chose Let's Encrypt IP certificate option echo -e "${green}Using Let's Encrypt for IP certificate (shortlived profile)...${plain}" # Ask for optional IPv6 local ipv6_addr="" read -rp "Do you have an IPv6 address to include? (leave empty to skip): " ipv6_addr ipv6_addr="${ipv6_addr// /}" # Trim whitespace # Stop panel if running (port 80 needed) if [[ $release == "alpine" ]]; then rc-service x-ui stop >/dev/null 2>&1 else systemctl stop x-ui >/dev/null 2>&1 fi setup_ip_certificate "${server_ip}" "${ipv6_addr}" if [ $? -eq 0 ]; then SSL_HOST="${server_ip}" echo -e "${green}✓ Let's Encrypt IP certificate configured successfully${plain}" else echo -e "${red}✗ IP certificate setup failed. Please check port 80 is open.${plain}" SSL_HOST="${server_ip}" fi ;; 3) # User chose Custom Paths (User Provided) option echo -e "${green}Using custom existing certificate...${plain}" local custom_cert="" local custom_key="" local custom_domain="" # 3.1 Request Domain to compose Panel URL later read -rp "Please enter domain name certificate issued for: " custom_domain custom_domain="${custom_domain// /}" # Убираем пробелы # 3.2 Loop for Certificate Path while true; do read -rp "Input certificate path (keywords: .crt / fullchain): " custom_cert # Strip quotes if present custom_cert=$(echo "$custom_cert" | tr -d '"' | tr -d "'") if [[ -f "$custom_cert" && -r "$custom_cert" && -s "$custom_cert" ]]; then break elif [[ ! -f "$custom_cert" ]]; then echo -e "${red}Error: File does not exist! Try again.${plain}" elif [[ ! -r "$custom_cert" ]]; then echo -e "${red}Error: File exists but is not readable (check permissions)!${plain}" else echo -e "${red}Error: File is empty!${plain}" fi done # 3.3 Loop for Private Key Path while true; do read -rp "Input private key path (keywords: .key / privatekey): " custom_key # Strip quotes if present custom_key=$(echo "$custom_key" | tr -d '"' | tr -d "'") if [[ -f "$custom_key" && -r "$custom_key" && -s "$custom_key" ]]; then break elif [[ ! -f "$custom_key" ]]; then echo -e "${red}Error: File does not exist! Try again.${plain}" elif [[ ! -r "$custom_key" ]]; then echo -e "${red}Error: File exists but is not readable (check permissions)!${plain}" else echo -e "${red}Error: File is empty!${plain}" fi done # 3.4 Apply Settings via x-ui binary ${xui_folder}/x-ui cert -webCert "$custom_cert" -webCertKey "$custom_key" >/dev/null 2>&1 # Set SSL_HOST for composing Panel URL if [[ -n "$custom_domain" ]]; then SSL_HOST="$custom_domain" else SSL_HOST="${server_ip}" fi echo -e "${green}✓ Custom certificate paths applied.${plain}" echo -e "${yellow}Note: You are responsible for renewing these files externally.${plain}" systemctl restart x-ui >/dev/null 2>&1 || rc-service x-ui restart >/dev/null 2>&1 ;; *) echo -e "${red}Invalid option. Skipping SSL setup.${plain}" SSL_HOST="${server_ip}" ;; esac } config_after_install() { local existing_hasDefaultCredential=$(${xui_folder}/x-ui setting -show true | grep -Eo 'hasDefaultCredential: .+' | awk '{print $2}') local existing_webBasePath=$(${xui_folder}/x-ui setting -show true | grep -Eo 'webBasePath: .+' | awk '{print $2}' | sed 's#^/##') local existing_port=$(${xui_folder}/x-ui setting -show true | grep -Eo 'port: .+' | awk '{print $2}') # Properly detect empty cert by checking if cert: line exists and has content after it local existing_cert=$(${xui_folder}/x-ui setting -getCert true | grep 'cert:' | awk -F': ' '{print $2}' | tr -d '[:space:]') local URL_lists=( "https://api4.ipify.org" "https://ipv4.icanhazip.com" "https://v4.api.ipinfo.io/ip" "https://ipv4.myexternalip.com/raw" "https://4.ident.me" "https://check-host.net/ip" ) local server_ip="" for ip_address in "${URL_lists[@]}"; do local response=$(curl -s -w "\n%{http_code}" --max-time 3 "${ip_address}" 2>/dev/null) local http_code=$(echo "$response" | tail -n1) local ip_result=$(echo "$response" | head -n-1 | tr -d '[:space:]') if [[ "${http_code}" == "200" && -n "${ip_result}" ]]; then server_ip="${ip_result}" break fi done if [[ ${#existing_webBasePath} -lt 4 ]]; then if [[ "$existing_hasDefaultCredential" == "true" ]]; then local config_webBasePath=$(gen_random_string 18) local config_username=$(gen_random_string 10) local config_password=$(gen_random_string 10) read -rp "Would you like to customize the Panel Port settings? (If not, a random port will be applied) [y/n]: " config_confirm if [[ "${config_confirm}" == "y" || "${config_confirm}" == "Y" ]]; then read -rp "Please set up the panel port: " config_port echo -e "${yellow}Your Panel Port is: ${config_port}${plain}" else local config_port=$(shuf -i 1024-62000 -n 1) echo -e "${yellow}Generated random port: ${config_port}${plain}" fi ${xui_folder}/x-ui setting -username "${config_username}" -password "${config_password}" -port "${config_port}" -webBasePath "${config_webBasePath}" echo "" echo -e "${green}═══════════════════════════════════════════${plain}" echo -e "${green} SSL Certificate Setup (MANDATORY) ${plain}" echo -e "${green}═══════════════════════════════════════════${plain}" echo -e "${yellow}For security, SSL certificate is required for all panels.${plain}" echo -e "${yellow}Let's Encrypt now supports both domains and IP addresses!${plain}" echo "" prompt_and_setup_ssl "${config_port}" "${config_webBasePath}" "${server_ip}" # Display final credentials and access information echo "" echo -e "${green}═══════════════════════════════════════════${plain}" echo -e "${green} Panel Installation Complete! ${plain}" echo -e "${green}═══════════════════════════════════════════${plain}" echo -e "${green}Username: ${config_username}${plain}" echo -e "${green}Password: ${config_password}${plain}" echo -e "${green}Port: ${config_port}${plain}" echo -e "${green}WebBasePath: ${config_webBasePath}${plain}" echo -e "${green}Access URL: https://${SSL_HOST}:${config_port}/${config_webBasePath}${plain}" echo -e "${green}═══════════════════════════════════════════${plain}" echo -e "${yellow}⚠ IMPORTANT: Save these credentials securely!${plain}" echo -e "${yellow}⚠ SSL Certificate: Enabled and configured${plain}" else local config_webBasePath=$(gen_random_string 18) echo -e "${yellow}WebBasePath is missing or too short. Generating a new one...${plain}" ${xui_folder}/x-ui setting -webBasePath "${config_webBasePath}" echo -e "${green}New WebBasePath: ${config_webBasePath}${plain}" # If the panel is already installed but no certificate is configured, prompt for SSL now if [[ -z "${existing_cert}" ]]; then echo "" echo -e "${green}═══════════════════════════════════════════${plain}" echo -e "${green} SSL Certificate Setup (RECOMMENDED) ${plain}" echo -e "${green}═══════════════════════════════════════════${plain}" echo -e "${yellow}Let's Encrypt now supports both domains and IP addresses!${plain}" echo "" prompt_and_setup_ssl "${existing_port}" "${config_webBasePath}" "${server_ip}" echo -e "${green}Access URL: https://${SSL_HOST}:${existing_port}/${config_webBasePath}${plain}" else # If a cert already exists, just show the access URL echo -e "${green}Access URL: https://${server_ip}:${existing_port}/${config_webBasePath}${plain}" fi fi else if [[ "$existing_hasDefaultCredential" == "true" ]]; then local config_username=$(gen_random_string 10) local config_password=$(gen_random_string 10) echo -e "${yellow}Default credentials detected. Security update required...${plain}" ${xui_folder}/x-ui setting -username "${config_username}" -password "${config_password}" echo -e "Generated new random login credentials:" echo -e "###############################################" echo -e "${green}Username: ${config_username}${plain}" echo -e "${green}Password: ${config_password}${plain}" echo -e "###############################################" else echo -e "${green}Username, Password, and WebBasePath are properly set.${plain}" fi # Existing install: if no cert configured, prompt user for SSL setup # Properly detect empty cert by checking if cert: line exists and has content after it existing_cert=$(${xui_folder}/x-ui setting -getCert true | grep 'cert:' | awk -F': ' '{print $2}' | tr -d '[:space:]') if [[ -z "$existing_cert" ]]; then echo "" echo -e "${green}═══════════════════════════════════════════${plain}" echo -e "${green} SSL Certificate Setup (RECOMMENDED) ${plain}" echo -e "${green}═══════════════════════════════════════════${plain}" echo -e "${yellow}Let's Encrypt now supports both domains and IP addresses!${plain}" echo "" prompt_and_setup_ssl "${existing_port}" "${existing_webBasePath}" "${server_ip}" echo -e "${green}Access URL: https://${SSL_HOST}:${existing_port}/${existing_webBasePath}${plain}" else echo -e "${green}SSL certificate already configured. No action needed.${plain}" fi fi ${xui_folder}/x-ui migrate } install_x-ui() { cd ${xui_folder%/x-ui}/ # Download resources if [ $# == 0 ]; then tag_version=$(curl -Ls "https://api.github.com/repos/MHSanaei/3x-ui/releases/latest" | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/') if [[ ! -n "$tag_version" ]]; then echo -e "${yellow}Trying to fetch version with IPv4...${plain}" tag_version=$(curl -4 -Ls "https://api.github.com/repos/MHSanaei/3x-ui/releases/latest" | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/') if [[ ! -n "$tag_version" ]]; then echo -e "${red}Failed to fetch x-ui version, it may be due to GitHub API restrictions, please try it later${plain}" exit 1 fi fi echo -e "Got x-ui latest version: ${tag_version}, beginning the installation..." curl -4fLRo ${xui_folder}-linux-$(arch).tar.gz https://github.com/MHSanaei/3x-ui/releases/download/${tag_version}/x-ui-linux-$(arch).tar.gz if [[ $? -ne 0 ]]; then echo -e "${red}Downloading x-ui failed, please be sure that your server can access GitHub ${plain}" exit 1 fi else tag_version=$1 tag_version_numeric=${tag_version#v} min_version="2.3.5" if [[ "$(printf '%s\n' "$min_version" "$tag_version_numeric" | sort -V | head -n1)" != "$min_version" ]]; then echo -e "${red}Please use a newer version (at least v2.3.5). Exiting installation.${plain}" exit 1 fi url="https://github.com/MHSanaei/3x-ui/releases/download/${tag_version}/x-ui-linux-$(arch).tar.gz" echo -e "Beginning to install x-ui $1" curl -4fLRo ${xui_folder}-linux-$(arch).tar.gz ${url} if [[ $? -ne 0 ]]; then echo -e "${red}Download x-ui $1 failed, please check if the version exists ${plain}" exit 1 fi fi curl -4fLRo /usr/bin/x-ui-temp https://raw.githubusercontent.com/MHSanaei/3x-ui/main/x-ui.sh if [[ $? -ne 0 ]]; then echo -e "${red}Failed to download x-ui.sh${plain}" exit 1 fi # Stop x-ui service and remove old resources if [[ -e ${xui_folder}/ ]]; then if [[ $release == "alpine" ]]; then rc-service x-ui stop else systemctl stop x-ui fi rm ${xui_folder}/ -rf fi # Extract resources and set permissions tar zxvf x-ui-linux-$(arch).tar.gz rm x-ui-linux-$(arch).tar.gz -f cd x-ui chmod +x x-ui chmod +x x-ui.sh # Check the system's architecture and rename the file accordingly if [[ $(arch) == "armv5" || $(arch) == "armv6" || $(arch) == "armv7" ]]; then mv bin/xray-linux-$(arch) bin/xray-linux-arm chmod +x bin/xray-linux-arm fi chmod +x x-ui bin/xray-linux-$(arch) # Update x-ui cli and se set permission mv -f /usr/bin/x-ui-temp /usr/bin/x-ui chmod +x /usr/bin/x-ui mkdir -p /var/log/x-ui config_after_install # Etckeeper compatibility if [ -d "/etc/.git" ]; then if [ -f "/etc/.gitignore" ]; then if ! grep -q "x-ui/x-ui.db" "/etc/.gitignore"; then echo "" >> "/etc/.gitignore" echo "x-ui/x-ui.db" >> "/etc/.gitignore" echo -e "${green}Added x-ui.db to /etc/.gitignore for etckeeper${plain}" fi else echo "x-ui/x-ui.db" > "/etc/.gitignore" echo -e "${green}Created /etc/.gitignore and added x-ui.db for etckeeper${plain}" fi fi if [[ $release == "alpine" ]]; then curl -4fLRo /etc/init.d/x-ui https://raw.githubusercontent.com/MHSanaei/3x-ui/main/x-ui.rc if [[ $? -ne 0 ]]; then echo -e "${red}Failed to download x-ui.rc${plain}" exit 1 fi chmod +x /etc/init.d/x-ui rc-update add x-ui rc-service x-ui start else # Install systemd service file service_installed=false if [ -f "x-ui.service" ]; then echo -e "${green}Found x-ui.service in extracted files, installing...${plain}" cp -f x-ui.service ${xui_service}/ >/dev/null 2>&1 if [[ $? -eq 0 ]]; then service_installed=true fi fi if [ "$service_installed" = false ]; then case "${release}" in ubuntu | debian | armbian) if [ -f "x-ui.service.debian" ]; then echo -e "${green}Found x-ui.service.debian in extracted files, installing...${plain}" cp -f x-ui.service.debian ${xui_service}/x-ui.service >/dev/null 2>&1 if [[ $? -eq 0 ]]; then service_installed=true fi fi ;; arch | manjaro | parch) if [ -f "x-ui.service.arch" ]; then echo -e "${green}Found x-ui.service.arch in extracted files, installing...${plain}" cp -f x-ui.service.arch ${xui_service}/x-ui.service >/dev/null 2>&1 if [[ $? -eq 0 ]]; then service_installed=true fi fi ;; *) if [ -f "x-ui.service.rhel" ]; then echo -e "${green}Found x-ui.service.rhel in extracted files, installing...${plain}" cp -f x-ui.service.rhel ${xui_service}/x-ui.service >/dev/null 2>&1 if [[ $? -eq 0 ]]; then service_installed=true fi fi ;; esac fi # If service file not found in tar.gz, download from GitHub if [ "$service_installed" = false ]; then echo -e "${yellow}Service files not found in tar.gz, downloading from GitHub...${plain}" case "${release}" in ubuntu | debian | armbian) curl -4fLRo ${xui_service}/x-ui.service https://raw.githubusercontent.com/MHSanaei/3x-ui/main/x-ui.service.debian >/dev/null 2>&1 ;; arch | manjaro | parch) curl -4fLRo ${xui_service}/x-ui.service https://raw.githubusercontent.com/MHSanaei/3x-ui/main/x-ui.service.arch >/dev/null 2>&1 ;; *) curl -4fLRo ${xui_service}/x-ui.service https://raw.githubusercontent.com/MHSanaei/3x-ui/main/x-ui.service.rhel >/dev/null 2>&1 ;; esac if [[ $? -ne 0 ]]; then echo -e "${red}Failed to install x-ui.service from GitHub${plain}" exit 1 fi service_installed=true fi if [ "$service_installed" = true ]; then echo -e "${green}Setting up systemd unit...${plain}" chown root:root ${xui_service}/x-ui.service >/dev/null 2>&1 chmod 644 ${xui_service}/x-ui.service >/dev/null 2>&1 systemctl daemon-reload systemctl enable x-ui systemctl start x-ui else echo -e "${red}Failed to install x-ui.service file${plain}" exit 1 fi fi echo -e "${green}x-ui ${tag_version}${plain} installation finished, it is running now..." echo -e "" echo -e "┌───────────────────────────────────────────────────────┐ │ ${blue}x-ui control menu usages (subcommands):${plain} │ │ │ │ ${blue}x-ui${plain} - Admin Management Script │ │ ${blue}x-ui start${plain} - Start │ │ ${blue}x-ui stop${plain} - Stop │ │ ${blue}x-ui restart${plain} - Restart │ │ ${blue}x-ui status${plain} - Current Status │ │ ${blue}x-ui settings${plain} - Current Settings │ │ ${blue}x-ui enable${plain} - Enable Autostart on OS Startup │ │ ${blue}x-ui disable${plain} - Disable Autostart on OS Startup │ │ ${blue}x-ui log${plain} - Check logs │ │ ${blue}x-ui banlog${plain} - Check Fail2ban ban logs │ │ ${blue}x-ui update${plain} - Update │ │ ${blue}x-ui legacy${plain} - Legacy version │ │ ${blue}x-ui install${plain} - Install │ │ ${blue}x-ui uninstall${plain} - Uninstall │ └───────────────────────────────────────────────────────┘" } echo -e "${green}Running...${plain}" install_base install_x-ui $1 ================================================ FILE: logger/logger.go ================================================ // Package logger provides logging functionality for the 3x-ui panel with // dual-backend logging (console/syslog and file) and buffered log storage for web UI. package logger import ( "fmt" "os" "path/filepath" "runtime" "time" "github.com/mhsanaei/3x-ui/v2/config" "github.com/op/go-logging" ) const ( maxLogBufferSize = 10240 // Maximum log entries kept in memory logFileName = "3xui.log" // Log file name timeFormat = "2006/01/02 15:04:05" // Log timestamp format ) var ( logger *logging.Logger logFile *os.File // logBuffer maintains recent log entries in memory for web UI retrieval logBuffer []struct { time string level logging.Level log string } ) // InitLogger initializes dual logging backends: console/syslog and file. // Console logging uses the specified level, file logging always uses DEBUG level. func InitLogger(level logging.Level) { newLogger := logging.MustGetLogger("x-ui") backends := make([]logging.Backend, 0, 2) // Console/syslog backend with configurable level if consoleBackend := initDefaultBackend(); consoleBackend != nil { leveledBackend := logging.AddModuleLevel(consoleBackend) leveledBackend.SetLevel(level, "x-ui") backends = append(backends, leveledBackend) } // File backend with DEBUG level for comprehensive logging if fileBackend := initFileBackend(); fileBackend != nil { leveledBackend := logging.AddModuleLevel(fileBackend) leveledBackend.SetLevel(logging.DEBUG, "x-ui") backends = append(backends, leveledBackend) } multiBackend := logging.MultiLogger(backends...) newLogger.SetBackend(multiBackend) logger = newLogger } // initDefaultBackend creates the console/syslog logging backend. // Windows: Uses stderr directly (no syslog support) // Unix-like: Attempts syslog, falls back to stderr func initDefaultBackend() logging.Backend { var backend logging.Backend includeTime := false if runtime.GOOS == "windows" { // Windows: Use stderr directly (no syslog support) backend = logging.NewLogBackend(os.Stderr, "", 0) includeTime = true } else { // Unix-like: Try syslog, fallback to stderr if syslogBackend, err := logging.NewSyslogBackend(""); err != nil { fmt.Fprintf(os.Stderr, "syslog backend disabled: %v\n", err) backend = logging.NewLogBackend(os.Stderr, "", 0) includeTime = os.Getppid() > 0 } else { backend = syslogBackend } } return logging.NewBackendFormatter(backend, newFormatter(includeTime)) } // initFileBackend creates the file logging backend. // Creates log directory and truncates log file on startup for fresh logs. func initFileBackend() logging.Backend { logDir := config.GetLogFolder() if err := os.MkdirAll(logDir, 0o750); err != nil { fmt.Fprintf(os.Stderr, "failed to create log folder %s: %v\n", logDir, err) return nil } logPath := filepath.Join(logDir, logFileName) file, err := os.OpenFile(logPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0o660) if err != nil { fmt.Fprintf(os.Stderr, "failed to open log file %s: %v\n", logPath, err) return nil } // Close previous log file if exists if logFile != nil { _ = logFile.Close() } logFile = file backend := logging.NewLogBackend(file, "", 0) return logging.NewBackendFormatter(backend, newFormatter(true)) } // newFormatter creates a log formatter with optional timestamp. func newFormatter(withTime bool) logging.Formatter { format := `%{level} - %{message}` if withTime { format = `%{time:` + timeFormat + `} %{level} - %{message}` } return logging.MustStringFormatter(format) } // CloseLogger closes the log file and cleans up resources. // Should be called during application shutdown. func CloseLogger() { if logFile != nil { _ = logFile.Close() logFile = nil } } // Debug logs a debug message and adds it to the log buffer. func Debug(args ...any) { logger.Debug(args...) addToBuffer("DEBUG", fmt.Sprint(args...)) } // Debugf logs a formatted debug message and adds it to the log buffer. func Debugf(format string, args ...any) { logger.Debugf(format, args...) addToBuffer("DEBUG", fmt.Sprintf(format, args...)) } // Info logs an info message and adds it to the log buffer. func Info(args ...any) { logger.Info(args...) addToBuffer("INFO", fmt.Sprint(args...)) } // Infof logs a formatted info message and adds it to the log buffer. func Infof(format string, args ...any) { logger.Infof(format, args...) addToBuffer("INFO", fmt.Sprintf(format, args...)) } // Notice logs a notice message and adds it to the log buffer. func Notice(args ...any) { logger.Notice(args...) addToBuffer("NOTICE", fmt.Sprint(args...)) } // Noticef logs a formatted notice message and adds it to the log buffer. func Noticef(format string, args ...any) { logger.Noticef(format, args...) addToBuffer("NOTICE", fmt.Sprintf(format, args...)) } // Warning logs a warning message and adds it to the log buffer. func Warning(args ...any) { logger.Warning(args...) addToBuffer("WARNING", fmt.Sprint(args...)) } // Warningf logs a formatted warning message and adds it to the log buffer. func Warningf(format string, args ...any) { logger.Warningf(format, args...) addToBuffer("WARNING", fmt.Sprintf(format, args...)) } // Error logs an error message and adds it to the log buffer. func Error(args ...any) { logger.Error(args...) addToBuffer("ERROR", fmt.Sprint(args...)) } // Errorf logs a formatted error message and adds it to the log buffer. func Errorf(format string, args ...any) { logger.Errorf(format, args...) addToBuffer("ERROR", fmt.Sprintf(format, args...)) } // addToBuffer adds a log entry to the in-memory ring buffer for web UI retrieval. func addToBuffer(level string, newLog string) { t := time.Now() if len(logBuffer) >= maxLogBufferSize { logBuffer = logBuffer[1:] } logLevel, _ := logging.LogLevel(level) logBuffer = append(logBuffer, struct { time string level logging.Level log string }{ time: t.Format(timeFormat), level: logLevel, log: newLog, }) } // GetLogs retrieves up to c log entries from the buffer that are at or below the specified level. func GetLogs(c int, level string) []string { var output []string logLevel, _ := logging.LogLevel(level) for i := len(logBuffer) - 1; i >= 0 && len(output) <= c; i-- { if logBuffer[i].level <= logLevel { output = append(output, fmt.Sprintf("%s %s - %s", logBuffer[i].time, logBuffer[i].level, logBuffer[i].log)) } } return output } ================================================ FILE: main.go ================================================ // Package main is the entry point for the 3x-ui web panel application. // It initializes the database, web server, and handles command-line operations for managing the panel. package main import ( "flag" "fmt" "log" "os" "os/signal" "syscall" _ "unsafe" "github.com/mhsanaei/3x-ui/v2/config" "github.com/mhsanaei/3x-ui/v2/database" "github.com/mhsanaei/3x-ui/v2/logger" "github.com/mhsanaei/3x-ui/v2/sub" "github.com/mhsanaei/3x-ui/v2/util/crypto" "github.com/mhsanaei/3x-ui/v2/util/sys" "github.com/mhsanaei/3x-ui/v2/web" "github.com/mhsanaei/3x-ui/v2/web/global" "github.com/mhsanaei/3x-ui/v2/web/service" "github.com/joho/godotenv" "github.com/op/go-logging" ) // runWebServer initializes and starts the web server for the 3x-ui panel. func runWebServer() { log.Printf("Starting %v %v", config.GetName(), config.GetVersion()) switch config.GetLogLevel() { case config.Debug: logger.InitLogger(logging.DEBUG) case config.Info: logger.InitLogger(logging.INFO) case config.Notice: logger.InitLogger(logging.NOTICE) case config.Warning: logger.InitLogger(logging.WARNING) case config.Error: logger.InitLogger(logging.ERROR) default: log.Fatalf("Unknown log level: %v", config.GetLogLevel()) } godotenv.Load() err := database.InitDB(config.GetDBPath()) if err != nil { log.Fatalf("Error initializing database: %v", err) } var server *web.Server server = web.NewServer() global.SetWebServer(server) err = server.Start() if err != nil { log.Fatalf("Error starting web server: %v", err) return } var subServer *sub.Server subServer = sub.NewServer() global.SetSubServer(subServer) err = subServer.Start() if err != nil { log.Fatalf("Error starting sub server: %v", err) return } sigCh := make(chan os.Signal, 1) // Trap shutdown signals signal.Notify(sigCh, syscall.SIGHUP, syscall.SIGTERM, sys.SIGUSR1) for { sig := <-sigCh switch sig { case syscall.SIGHUP: logger.Info("Received SIGHUP signal. Restarting servers...") // --- FIX FOR TELEGRAM BOT CONFLICT (409): Stop bot before restart --- service.StopBot() // -- err := server.Stop() if err != nil { logger.Debug("Error stopping web server:", err) } err = subServer.Stop() if err != nil { logger.Debug("Error stopping sub server:", err) } server = web.NewServer() global.SetWebServer(server) err = server.Start() if err != nil { log.Fatalf("Error restarting web server: %v", err) return } log.Println("Web server restarted successfully.") subServer = sub.NewServer() global.SetSubServer(subServer) err = subServer.Start() if err != nil { log.Fatalf("Error restarting sub server: %v", err) return } log.Println("Sub server restarted successfully.") case sys.SIGUSR1: logger.Info("Received USR1 signal, restarting xray-core...") err := server.RestartXray() if err != nil { logger.Error("Failed to restart xray-core:", err) } default: // --- FIX FOR TELEGRAM BOT CONFLICT (409) on full shutdown --- service.StopBot() // ------------------------------------------------------------ server.Stop() subServer.Stop() log.Println("Shutting down servers.") return } } } // resetSetting resets all panel settings to their default values. func resetSetting() { err := database.InitDB(config.GetDBPath()) if err != nil { fmt.Println("Failed to initialize database:", err) return } settingService := service.SettingService{} err = settingService.ResetSettings() if err != nil { fmt.Println("Failed to reset settings:", err) } else { fmt.Println("Settings successfully reset.") } } // showSetting displays the current panel settings if show is true. func showSetting(show bool) { if show { settingService := service.SettingService{} port, err := settingService.GetPort() if err != nil { fmt.Println("get current port failed, error info:", err) } webBasePath, err := settingService.GetBasePath() if err != nil { fmt.Println("get webBasePath failed, error info:", err) } certFile, err := settingService.GetCertFile() if err != nil { fmt.Println("get cert file failed, error info:", err) } keyFile, err := settingService.GetKeyFile() if err != nil { fmt.Println("get key file failed, error info:", err) } userService := service.UserService{} userModel, err := userService.GetFirstUser() if err != nil { fmt.Println("get current user info failed, error info:", err) } if userModel.Username == "" || userModel.Password == "" { fmt.Println("current username or password is empty") } fmt.Println("current panel settings as follows:") if certFile == "" || keyFile == "" { fmt.Println("Warning: Panel is not secure with SSL") } else { fmt.Println("Panel is secure with SSL") } hasDefaultCredential := func() bool { return userModel.Username == "admin" && crypto.CheckPasswordHash(userModel.Password, "admin") }() fmt.Println("hasDefaultCredential:", hasDefaultCredential) fmt.Println("port:", port) fmt.Println("webBasePath:", webBasePath) } } // updateTgbotEnableSts enables or disables the Telegram bot notifications based on the status parameter. func updateTgbotEnableSts(status bool) { settingService := service.SettingService{} currentTgSts, err := settingService.GetTgbotEnabled() if err != nil { fmt.Println(err) return } logger.Infof("current enabletgbot status[%v],need update to status[%v]", currentTgSts, status) if currentTgSts != status { err := settingService.SetTgbotEnabled(status) if err != nil { fmt.Println(err) return } else { logger.Infof("SetTgbotEnabled[%v] success", status) } } } // updateTgbotSetting updates Telegram bot settings including token, chat ID, and runtime schedule. func updateTgbotSetting(tgBotToken string, tgBotChatid string, tgBotRuntime string) { err := database.InitDB(config.GetDBPath()) if err != nil { fmt.Println("Error initializing database:", err) return } settingService := service.SettingService{} if tgBotToken != "" { err := settingService.SetTgBotToken(tgBotToken) if err != nil { fmt.Printf("Error setting Telegram bot token: %v\n", err) return } logger.Info("Successfully updated Telegram bot token.") } if tgBotRuntime != "" { err := settingService.SetTgbotRuntime(tgBotRuntime) if err != nil { fmt.Printf("Error setting Telegram bot runtime: %v\n", err) return } logger.Infof("Successfully updated Telegram bot runtime to [%s].", tgBotRuntime) } if tgBotChatid != "" { err := settingService.SetTgBotChatId(tgBotChatid) if err != nil { fmt.Printf("Error setting Telegram bot chat ID: %v\n", err) return } logger.Info("Successfully updated Telegram bot chat ID.") } } // updateSetting updates various panel settings including port, credentials, base path, listen IP, and two-factor authentication. func updateSetting(port int, username string, password string, webBasePath string, listenIP string, resetTwoFactor bool) { err := database.InitDB(config.GetDBPath()) if err != nil { fmt.Println("Database initialization failed:", err) return } settingService := service.SettingService{} userService := service.UserService{} if port > 0 { err := settingService.SetPort(port) if err != nil { fmt.Println("Failed to set port:", err) } else { fmt.Printf("Port set successfully: %v\n", port) } } if username != "" || password != "" { err := userService.UpdateFirstUser(username, password) if err != nil { fmt.Println("Failed to update username and password:", err) } else { fmt.Println("Username and password updated successfully") } } if webBasePath != "" { err := settingService.SetBasePath(webBasePath) if err != nil { fmt.Println("Failed to set base URI path:", err) } else { fmt.Println("Base URI path set successfully") } } if resetTwoFactor { err := settingService.SetTwoFactorEnable(false) if err != nil { fmt.Println("Failed to reset two-factor authentication:", err) } else { settingService.SetTwoFactorToken("") fmt.Println("Two-factor authentication reset successfully") } } if listenIP != "" { err := settingService.SetListen(listenIP) if err != nil { fmt.Println("Failed to set listen IP:", err) } else { fmt.Printf("listen %v set successfully", listenIP) } } } // updateCert updates the SSL certificate files for the panel. func updateCert(publicKey string, privateKey string) { err := database.InitDB(config.GetDBPath()) if err != nil { fmt.Println(err) return } if (privateKey != "" && publicKey != "") || (privateKey == "" && publicKey == "") { settingService := service.SettingService{} err = settingService.SetCertFile(publicKey) if err != nil { fmt.Println("set certificate public key failed:", err) } else { fmt.Println("set certificate public key success") } err = settingService.SetKeyFile(privateKey) if err != nil { fmt.Println("set certificate private key failed:", err) } else { fmt.Println("set certificate private key success") } err = settingService.SetSubCertFile(publicKey) if err != nil { fmt.Println("set certificate for subscription public key failed:", err) } else { fmt.Println("set certificate for subscription public key success") } err = settingService.SetSubKeyFile(privateKey) if err != nil { fmt.Println("set certificate for subscription private key failed:", err) } else { fmt.Println("set certificate for subscription private key success") } } else { fmt.Println("both public and private key should be entered.") } } // GetCertificate displays the current SSL certificate settings if getCert is true. func GetCertificate(getCert bool) { if getCert { settingService := service.SettingService{} certFile, err := settingService.GetCertFile() if err != nil { fmt.Println("get cert file failed, error info:", err) } keyFile, err := settingService.GetKeyFile() if err != nil { fmt.Println("get key file failed, error info:", err) } fmt.Println("cert:", certFile) fmt.Println("key:", keyFile) } } // GetListenIP displays the current panel listen IP address if getListen is true. func GetListenIP(getListen bool) { if getListen { settingService := service.SettingService{} ListenIP, err := settingService.GetListen() if err != nil { log.Printf("Failed to retrieve listen IP: %v", err) return } fmt.Println("listenIP:", ListenIP) } } // migrateDb performs database migration operations for the 3x-ui panel. func migrateDb() { inboundService := service.InboundService{} err := database.InitDB(config.GetDBPath()) if err != nil { log.Fatal(err) } fmt.Println("Start migrating database...") inboundService.MigrateDB() fmt.Println("Migration done!") } // main is the entry point of the 3x-ui application. // It parses command-line arguments to run the web server, migrate database, or update settings. func main() { if len(os.Args) < 2 { runWebServer() return } var showVersion bool flag.BoolVar(&showVersion, "v", false, "show version") runCmd := flag.NewFlagSet("run", flag.ExitOnError) settingCmd := flag.NewFlagSet("setting", flag.ExitOnError) var port int var username string var password string var webBasePath string var listenIP string var getListen bool var webCertFile string var webKeyFile string var tgbottoken string var tgbotchatid string var enabletgbot bool var tgbotRuntime string var reset bool var show bool var getCert bool var resetTwoFactor bool settingCmd.BoolVar(&reset, "reset", false, "Reset all settings") settingCmd.BoolVar(&show, "show", false, "Display current settings") settingCmd.IntVar(&port, "port", 0, "Set panel port number") settingCmd.StringVar(&username, "username", "", "Set login username") settingCmd.StringVar(&password, "password", "", "Set login password") settingCmd.StringVar(&webBasePath, "webBasePath", "", "Set base path for Panel") settingCmd.StringVar(&listenIP, "listenIP", "", "set panel listenIP IP") settingCmd.BoolVar(&resetTwoFactor, "resetTwoFactor", false, "Reset two-factor authentication settings") settingCmd.BoolVar(&getListen, "getListen", false, "Display current panel listenIP IP") settingCmd.BoolVar(&getCert, "getCert", false, "Display current certificate settings") settingCmd.StringVar(&webCertFile, "webCert", "", "Set path to public key file for panel") settingCmd.StringVar(&webKeyFile, "webCertKey", "", "Set path to private key file for panel") settingCmd.StringVar(&tgbottoken, "tgbottoken", "", "Set token for Telegram bot") settingCmd.StringVar(&tgbotRuntime, "tgbotRuntime", "", "Set cron time for Telegram bot notifications") settingCmd.StringVar(&tgbotchatid, "tgbotchatid", "", "Set chat ID for Telegram bot notifications") settingCmd.BoolVar(&enabletgbot, "enabletgbot", false, "Enable notifications via Telegram bot") oldUsage := flag.Usage flag.Usage = func() { oldUsage() fmt.Println() fmt.Println("Commands:") fmt.Println(" run run web panel") fmt.Println(" migrate migrate form other/old x-ui") fmt.Println(" setting set settings") } flag.Parse() if showVersion { fmt.Println(config.GetVersion()) return } switch os.Args[1] { case "run": err := runCmd.Parse(os.Args[2:]) if err != nil { fmt.Println(err) return } runWebServer() case "migrate": migrateDb() case "setting": err := settingCmd.Parse(os.Args[2:]) if err != nil { fmt.Println(err) return } if reset { resetSetting() } else { updateSetting(port, username, password, webBasePath, listenIP, resetTwoFactor) } if show { showSetting(show) } if getListen { GetListenIP(getListen) } if getCert { GetCertificate(getCert) } if (tgbottoken != "") || (tgbotchatid != "") || (tgbotRuntime != "") { updateTgbotSetting(tgbottoken, tgbotchatid, tgbotRuntime) } if enabletgbot { updateTgbotEnableSts(enabletgbot) } case "cert": err := settingCmd.Parse(os.Args[2:]) if err != nil { fmt.Println(err) return } if reset { updateCert("", "") } else { updateCert(webCertFile, webKeyFile) } default: fmt.Println("Invalid subcommands") fmt.Println() runCmd.Usage() fmt.Println() settingCmd.Usage() } } ================================================ FILE: sub/default.json ================================================ { "remarks": "", "dns": { "tag": "dns_out", "queryStrategy": "UseIP", "servers": [ { "address": "8.8.8.8", "skipFallback": false } ] }, "inbounds": [ { "port": 10808, "protocol": "mixed", "settings": { "auth": "noauth", "udp": true, "userLevel": 8 }, "sniffing": { "destOverride": [ "http", "tls", "quic", "fakedns" ], "enabled": true }, "tag": "mixed" }, { "port": 10809, "protocol": "http", "settings": { "userLevel": 8 }, "tag": "http" } ], "log": { "loglevel": "warning" }, "outbounds": [ { "tag": "direct", "protocol": "freedom", "settings": { "domainStrategy": "AsIs", "redirect": "", "noises": [] } }, { "tag": "block", "protocol": "blackhole", "settings": { "response": { "type": "http" } } } ], "policy": { "levels": { "8": { "connIdle": 300, "downlinkOnly": 1, "handshake": 4, "uplinkOnly": 1 } }, "system": { "statsOutboundUplink": true, "statsOutboundDownlink": true } }, "routing": { "domainStrategy": "AsIs", "rules": [ { "type": "field", "network": "tcp,udp", "outboundTag": "proxy" } ] }, "stats": {} } ================================================ FILE: sub/sub.go ================================================ // Package sub provides subscription server functionality for the 3x-ui panel, // including HTTP/HTTPS servers for serving subscription links and JSON configurations. package sub import ( "context" "crypto/tls" "html/template" "io" "io/fs" "net" "net/http" "os" "path/filepath" "strconv" "strings" "github.com/mhsanaei/3x-ui/v2/logger" "github.com/mhsanaei/3x-ui/v2/util/common" webpkg "github.com/mhsanaei/3x-ui/v2/web" "github.com/mhsanaei/3x-ui/v2/web/locale" "github.com/mhsanaei/3x-ui/v2/web/middleware" "github.com/mhsanaei/3x-ui/v2/web/network" "github.com/mhsanaei/3x-ui/v2/web/service" "github.com/gin-gonic/gin" ) // setEmbeddedTemplates parses and sets embedded templates on the engine func setEmbeddedTemplates(engine *gin.Engine) error { t, err := template.New("").Funcs(engine.FuncMap).ParseFS( webpkg.EmbeddedHTML(), "html/common/page.html", "html/component/aThemeSwitch.html", "html/settings/panel/subscription/subpage.html", ) if err != nil { return err } engine.SetHTMLTemplate(t) return nil } // Server represents the subscription server that serves subscription links and JSON configurations. type Server struct { httpServer *http.Server listener net.Listener sub *SUBController settingService service.SettingService ctx context.Context cancel context.CancelFunc } // NewServer creates a new subscription server instance with a cancellable context. func NewServer() *Server { ctx, cancel := context.WithCancel(context.Background()) return &Server{ ctx: ctx, cancel: cancel, } } // initRouter configures the subscription server's Gin engine, middleware, // templates and static assets and returns the ready-to-use engine. func (s *Server) initRouter() (*gin.Engine, error) { // Always run in release mode for the subscription server gin.DefaultWriter = io.Discard gin.DefaultErrorWriter = io.Discard gin.SetMode(gin.ReleaseMode) engine := gin.Default() subDomain, err := s.settingService.GetSubDomain() if err != nil { return nil, err } if subDomain != "" { engine.Use(middleware.DomainValidatorMiddleware(subDomain)) } LinksPath, err := s.settingService.GetSubPath() if err != nil { return nil, err } JsonPath, err := s.settingService.GetSubJsonPath() if err != nil { return nil, err } // Determine if JSON subscription endpoint is enabled subJsonEnable, err := s.settingService.GetSubJsonEnable() if err != nil { return nil, err } // Set base_path based on LinksPath for template rendering // Ensure LinksPath ends with "/" for proper asset URL generation basePath := LinksPath if basePath != "/" && !strings.HasSuffix(basePath, "/") { basePath += "/" } // logger.Debug("sub: Setting base_path to:", basePath) engine.Use(func(c *gin.Context) { c.Set("base_path", basePath) }) Encrypt, err := s.settingService.GetSubEncrypt() if err != nil { return nil, err } ShowInfo, err := s.settingService.GetSubShowInfo() if err != nil { return nil, err } RemarkModel, err := s.settingService.GetRemarkModel() if err != nil { RemarkModel = "-ieo" } SubUpdates, err := s.settingService.GetSubUpdates() if err != nil { SubUpdates = "10" } SubJsonFragment, err := s.settingService.GetSubJsonFragment() if err != nil { SubJsonFragment = "" } SubJsonNoises, err := s.settingService.GetSubJsonNoises() if err != nil { SubJsonNoises = "" } SubJsonMux, err := s.settingService.GetSubJsonMux() if err != nil { SubJsonMux = "" } SubJsonRules, err := s.settingService.GetSubJsonRules() if err != nil { SubJsonRules = "" } SubTitle, err := s.settingService.GetSubTitle() if err != nil { SubTitle = "" } SubSupportUrl, err := s.settingService.GetSubSupportUrl() if err != nil { SubSupportUrl = "" } SubProfileUrl, err := s.settingService.GetSubProfileUrl() if err != nil { SubProfileUrl = "" } SubAnnounce, err := s.settingService.GetSubAnnounce() if err != nil { SubAnnounce = "" } SubEnableRouting, err := s.settingService.GetSubEnableRouting() if err != nil { return nil, err } SubRoutingRules, err := s.settingService.GetSubRoutingRules() if err != nil { SubRoutingRules = "" } // set per-request localizer from headers/cookies engine.Use(locale.LocalizerMiddleware()) // register i18n function similar to web server i18nWebFunc := func(key string, params ...string) string { return locale.I18n(locale.Web, key, params...) } engine.SetFuncMap(map[string]any{"i18n": i18nWebFunc}) // Templates: prefer embedded; fallback to disk if necessary if err := setEmbeddedTemplates(engine); err != nil { logger.Warning("sub: failed to parse embedded templates:", err) if files, derr := s.getHtmlFiles(); derr == nil { engine.LoadHTMLFiles(files...) } else { logger.Error("sub: no templates available (embedded parse and disk load failed)", err, derr) } } // Assets: use disk if present, fallback to embedded // Serve under both root (/assets) and under the subscription path prefix (LinksPath + "assets") // so reverse proxies with a URI prefix can load assets correctly. // Determine LinksPath earlier to compute prefixed assets mount. // Note: LinksPath always starts and ends with "/" (validated in settings). var linksPathForAssets string if LinksPath == "/" { linksPathForAssets = "/assets" } else { // ensure single slash join linksPathForAssets = strings.TrimRight(LinksPath, "/") + "/assets" } // Mount assets in multiple paths to handle different URL patterns var assetsFS http.FileSystem if _, err := os.Stat("web/assets"); err == nil { assetsFS = http.FS(os.DirFS("web/assets")) } else { if subFS, err := fs.Sub(webpkg.EmbeddedAssets(), "assets"); err == nil { assetsFS = http.FS(subFS) } else { logger.Error("sub: failed to mount embedded assets:", err) } } if assetsFS != nil { engine.StaticFS("/assets", assetsFS) if linksPathForAssets != "/assets" { engine.StaticFS(linksPathForAssets, assetsFS) } // Add middleware to handle dynamic asset paths with subid if LinksPath != "/" { engine.Use(func(c *gin.Context) { path := c.Request.URL.Path // Check if this is an asset request with subid pattern: /sub/path/{subid}/assets/... pathPrefix := strings.TrimRight(LinksPath, "/") + "/" if strings.HasPrefix(path, pathPrefix) && strings.Contains(path, "/assets/") { // Extract the asset path after /assets/ assetsIndex := strings.Index(path, "/assets/") if assetsIndex != -1 { assetPath := path[assetsIndex+8:] // +8 to skip "/assets/" if assetPath != "" { // Serve the asset file c.FileFromFS(assetPath, assetsFS) c.Abort() return } } } c.Next() }) } } g := engine.Group("/") s.sub = NewSUBController( g, LinksPath, JsonPath, subJsonEnable, Encrypt, ShowInfo, RemarkModel, SubUpdates, SubJsonFragment, SubJsonNoises, SubJsonMux, SubJsonRules, SubTitle, SubSupportUrl, SubProfileUrl, SubAnnounce, SubEnableRouting, SubRoutingRules) return engine, nil } // getHtmlFiles loads templates from local folder (used in debug mode) func (s *Server) getHtmlFiles() ([]string, error) { dir, _ := os.Getwd() files := []string{} // common layout common := filepath.Join(dir, "web", "html", "common", "page.html") if _, err := os.Stat(common); err == nil { files = append(files, common) } // components used theme := filepath.Join(dir, "web", "html", "component", "aThemeSwitch.html") if _, err := os.Stat(theme); err == nil { files = append(files, theme) } // page itself page := filepath.Join(dir, "web", "html", "subpage.html") if _, err := os.Stat(page); err == nil { files = append(files, page) } else { return nil, err } return files, nil } // Start initializes and starts the subscription server with configured settings. func (s *Server) Start() (err error) { // This is an anonymous function, no function name defer func() { if err != nil { s.Stop() } }() subEnable, err := s.settingService.GetSubEnable() if err != nil { return err } if !subEnable { return nil } engine, err := s.initRouter() if err != nil { return err } certFile, err := s.settingService.GetSubCertFile() if err != nil { return err } keyFile, err := s.settingService.GetSubKeyFile() if err != nil { return err } listen, err := s.settingService.GetSubListen() if err != nil { return err } port, err := s.settingService.GetSubPort() if err != nil { return err } listenAddr := net.JoinHostPort(listen, strconv.Itoa(port)) listener, err := net.Listen("tcp", listenAddr) if err != nil { return err } if certFile != "" || keyFile != "" { cert, err := tls.LoadX509KeyPair(certFile, keyFile) if err == nil { c := &tls.Config{ Certificates: []tls.Certificate{cert}, } listener = network.NewAutoHttpsListener(listener) listener = tls.NewListener(listener, c) logger.Info("Sub server running HTTPS on", listener.Addr()) } else { logger.Error("Error loading certificates:", err) logger.Info("Sub server running HTTP on", listener.Addr()) } } else { logger.Info("Sub server running HTTP on", listener.Addr()) } s.listener = listener s.httpServer = &http.Server{ Handler: engine, } go func() { s.httpServer.Serve(listener) }() return nil } // Stop gracefully shuts down the subscription server and closes the listener. func (s *Server) Stop() error { s.cancel() var err1 error var err2 error if s.httpServer != nil { err1 = s.httpServer.Shutdown(s.ctx) } if s.listener != nil { err2 = s.listener.Close() } return common.Combine(err1, err2) } // GetCtx returns the server's context for cancellation and deadline management. func (s *Server) GetCtx() context.Context { return s.ctx } ================================================ FILE: sub/subController.go ================================================ package sub import ( "encoding/base64" "fmt" "strconv" "strings" "github.com/mhsanaei/3x-ui/v2/config" "github.com/gin-gonic/gin" ) // SUBController handles HTTP requests for subscription links and JSON configurations. type SUBController struct { subTitle string subSupportUrl string subProfileUrl string subAnnounce string subEnableRouting bool subRoutingRules string subPath string subJsonPath string jsonEnabled bool subEncrypt bool updateInterval string subService *SubService subJsonService *SubJsonService } // NewSUBController creates a new subscription controller with the given configuration. func NewSUBController( g *gin.RouterGroup, subPath string, jsonPath string, jsonEnabled bool, encrypt bool, showInfo bool, rModel string, update string, jsonFragment string, jsonNoise string, jsonMux string, jsonRules string, subTitle string, subSupportUrl string, subProfileUrl string, subAnnounce string, subEnableRouting bool, subRoutingRules string, ) *SUBController { sub := NewSubService(showInfo, rModel) a := &SUBController{ subTitle: subTitle, subSupportUrl: subSupportUrl, subProfileUrl: subProfileUrl, subAnnounce: subAnnounce, subEnableRouting: subEnableRouting, subRoutingRules: subRoutingRules, subPath: subPath, subJsonPath: jsonPath, jsonEnabled: jsonEnabled, subEncrypt: encrypt, updateInterval: update, subService: sub, subJsonService: NewSubJsonService(jsonFragment, jsonNoise, jsonMux, jsonRules, sub), } a.initRouter(g) return a } // initRouter registers HTTP routes for subscription links and JSON endpoints // on the provided router group. func (a *SUBController) initRouter(g *gin.RouterGroup) { gLink := g.Group(a.subPath) gLink.GET(":subid", a.subs) if a.jsonEnabled { gJson := g.Group(a.subJsonPath) gJson.GET(":subid", a.subJsons) } } // subs handles HTTP requests for subscription links, returning either HTML page or base64-encoded subscription data. func (a *SUBController) subs(c *gin.Context) { subId := c.Param("subid") scheme, host, hostWithPort, hostHeader := a.subService.ResolveRequest(c) subs, lastOnline, traffic, err := a.subService.GetSubs(subId, host) if err != nil || len(subs) == 0 { c.String(400, "Error!") } else { result := "" for _, sub := range subs { result += sub + "\n" } // If the request expects HTML (e.g., browser) or explicitly asked (?html=1 or ?view=html), render the info page here accept := c.GetHeader("Accept") if strings.Contains(strings.ToLower(accept), "text/html") || c.Query("html") == "1" || strings.EqualFold(c.Query("view"), "html") { // Build page data in service subURL, subJsonURL := a.subService.BuildURLs(scheme, hostWithPort, a.subPath, a.subJsonPath, subId) if !a.jsonEnabled { subJsonURL = "" } // Get base_path from context (set by middleware) basePath, exists := c.Get("base_path") if !exists { basePath = "/" } // Add subId to base_path for asset URLs basePathStr := basePath.(string) if basePathStr == "/" { basePathStr = "/" + subId + "/" } else { // Remove trailing slash if exists, add subId, then add trailing slash basePathStr = strings.TrimRight(basePathStr, "/") + "/" + subId + "/" } page := a.subService.BuildPageData(subId, hostHeader, traffic, lastOnline, subs, subURL, subJsonURL, basePathStr) c.HTML(200, "subpage.html", gin.H{ "title": "subscription.title", "cur_ver": config.GetVersion(), "host": page.Host, "base_path": page.BasePath, "sId": page.SId, "download": page.Download, "upload": page.Upload, "total": page.Total, "used": page.Used, "remained": page.Remained, "expire": page.Expire, "lastOnline": page.LastOnline, "datepicker": page.Datepicker, "downloadByte": page.DownloadByte, "uploadByte": page.UploadByte, "totalByte": page.TotalByte, "subUrl": page.SubUrl, "subJsonUrl": page.SubJsonUrl, "result": page.Result, }) return } // Add headers header := fmt.Sprintf("upload=%d; download=%d; total=%d; expire=%d", traffic.Up, traffic.Down, traffic.Total, traffic.ExpiryTime/1000) profileUrl := a.subProfileUrl if profileUrl == "" { profileUrl = fmt.Sprintf("%s://%s%s", scheme, hostWithPort, c.Request.RequestURI) } a.ApplyCommonHeaders(c, header, a.updateInterval, a.subTitle, a.subSupportUrl, profileUrl, a.subAnnounce, a.subEnableRouting, a.subRoutingRules) if a.subEncrypt { c.String(200, base64.StdEncoding.EncodeToString([]byte(result))) } else { c.String(200, result) } } } // subJsons handles HTTP requests for JSON subscription configurations. func (a *SUBController) subJsons(c *gin.Context) { subId := c.Param("subid") scheme, host, hostWithPort, _ := a.subService.ResolveRequest(c) jsonSub, header, err := a.subJsonService.GetJson(subId, host) if err != nil || len(jsonSub) == 0 { c.String(400, "Error!") } else { // Add headers profileUrl := a.subProfileUrl if profileUrl == "" { profileUrl = fmt.Sprintf("%s://%s%s", scheme, hostWithPort, c.Request.RequestURI) } a.ApplyCommonHeaders(c, header, a.updateInterval, a.subTitle, a.subSupportUrl, profileUrl, a.subAnnounce, a.subEnableRouting, a.subRoutingRules) c.String(200, jsonSub) } } // ApplyCommonHeaders sets common HTTP headers for subscription responses including user info, update interval, and profile title. func (a *SUBController) ApplyCommonHeaders( c *gin.Context, header, updateInterval, profileTitle string, profileSupportUrl string, profileUrl string, profileAnnounce string, profileEnableRouting bool, profileRoutingRules string, ) { c.Writer.Header().Set("Subscription-Userinfo", header) c.Writer.Header().Set("Profile-Update-Interval", updateInterval) //Basics if profileTitle != "" { c.Writer.Header().Set("Profile-Title", "base64:"+base64.StdEncoding.EncodeToString([]byte(profileTitle))) } if profileSupportUrl != "" { c.Writer.Header().Set("Support-Url", profileSupportUrl) } if profileUrl != "" { c.Writer.Header().Set("Profile-Web-Page-Url", profileUrl) } if profileAnnounce != "" { c.Writer.Header().Set("Announce", "base64:"+base64.StdEncoding.EncodeToString([]byte(profileAnnounce))) } //Advanced (Happ) c.Writer.Header().Set("Routing-Enable", strconv.FormatBool(profileEnableRouting)) if profileRoutingRules != "" { c.Writer.Header().Set("Routing", profileRoutingRules) } } ================================================ FILE: sub/subJsonService.go ================================================ package sub import ( _ "embed" "encoding/json" "fmt" "maps" "strings" "github.com/mhsanaei/3x-ui/v2/database/model" "github.com/mhsanaei/3x-ui/v2/logger" "github.com/mhsanaei/3x-ui/v2/util/json_util" "github.com/mhsanaei/3x-ui/v2/util/random" "github.com/mhsanaei/3x-ui/v2/web/service" "github.com/mhsanaei/3x-ui/v2/xray" ) //go:embed default.json var defaultJson string // SubJsonService handles JSON subscription configuration generation and management. type SubJsonService struct { configJson map[string]any defaultOutbounds []json_util.RawMessage fragment string noises string mux string inboundService service.InboundService SubService *SubService } // NewSubJsonService creates a new JSON subscription service with the given configuration. func NewSubJsonService(fragment string, noises string, mux string, rules string, subService *SubService) *SubJsonService { var configJson map[string]any var defaultOutbounds []json_util.RawMessage json.Unmarshal([]byte(defaultJson), &configJson) if outboundSlices, ok := configJson["outbounds"].([]any); ok { for _, defaultOutbound := range outboundSlices { jsonBytes, _ := json.Marshal(defaultOutbound) defaultOutbounds = append(defaultOutbounds, jsonBytes) } } if rules != "" { var newRules []any routing, _ := configJson["routing"].(map[string]any) defaultRules, _ := routing["rules"].([]any) json.Unmarshal([]byte(rules), &newRules) defaultRules = append(newRules, defaultRules...) routing["rules"] = defaultRules configJson["routing"] = routing } if fragment != "" { defaultOutbounds = append(defaultOutbounds, json_util.RawMessage(fragment)) } if noises != "" { defaultOutbounds = append(defaultOutbounds, json_util.RawMessage(noises)) } return &SubJsonService{ configJson: configJson, defaultOutbounds: defaultOutbounds, fragment: fragment, noises: noises, mux: mux, SubService: subService, } } // GetJson generates a JSON subscription configuration for the given subscription ID and host. func (s *SubJsonService) GetJson(subId string, host string) (string, string, error) { inbounds, err := s.SubService.getInboundsBySubId(subId) if err != nil || len(inbounds) == 0 { return "", "", err } var header string var traffic xray.ClientTraffic var clientTraffics []xray.ClientTraffic var configArray []json_util.RawMessage // Prepare Inbounds for _, inbound := range inbounds { clients, err := s.inboundService.GetClients(inbound) if err != nil { logger.Error("SubJsonService - GetClients: Unable to get clients from inbound") } if clients == nil { continue } if len(inbound.Listen) > 0 && inbound.Listen[0] == '@' { listen, port, streamSettings, err := s.SubService.getFallbackMaster(inbound.Listen, inbound.StreamSettings) if err == nil { inbound.Listen = listen inbound.Port = port inbound.StreamSettings = streamSettings } } for _, client := range clients { if client.Enable && client.SubID == subId { clientTraffics = append(clientTraffics, s.SubService.getClientTraffics(inbound.ClientStats, client.Email)) newConfigs := s.getConfig(inbound, client, host) configArray = append(configArray, newConfigs...) } } } if len(configArray) == 0 { return "", "", nil } // Prepare statistics for index, clientTraffic := range clientTraffics { if index == 0 { traffic.Up = clientTraffic.Up traffic.Down = clientTraffic.Down traffic.Total = clientTraffic.Total if clientTraffic.ExpiryTime > 0 { traffic.ExpiryTime = clientTraffic.ExpiryTime } } else { traffic.Up += clientTraffic.Up traffic.Down += clientTraffic.Down if traffic.Total == 0 || clientTraffic.Total == 0 { traffic.Total = 0 } else { traffic.Total += clientTraffic.Total } if clientTraffic.ExpiryTime != traffic.ExpiryTime { traffic.ExpiryTime = 0 } } } // Combile outbounds var finalJson []byte if len(configArray) == 1 { finalJson, _ = json.MarshalIndent(configArray[0], "", " ") } else { finalJson, _ = json.MarshalIndent(configArray, "", " ") } header = fmt.Sprintf("upload=%d; download=%d; total=%d; expire=%d", traffic.Up, traffic.Down, traffic.Total, traffic.ExpiryTime/1000) return string(finalJson), header, nil } func (s *SubJsonService) getConfig(inbound *model.Inbound, client model.Client, host string) []json_util.RawMessage { var newJsonArray []json_util.RawMessage stream := s.streamData(inbound.StreamSettings) externalProxies, ok := stream["externalProxy"].([]any) if !ok || len(externalProxies) == 0 { externalProxies = []any{ map[string]any{ "forceTls": "same", "dest": host, "port": float64(inbound.Port), "remark": "", }, } } delete(stream, "externalProxy") for _, ep := range externalProxies { extPrxy := ep.(map[string]any) inbound.Listen = extPrxy["dest"].(string) inbound.Port = int(extPrxy["port"].(float64)) newStream := stream switch extPrxy["forceTls"].(string) { case "tls": if newStream["security"] != "tls" { newStream["security"] = "tls" newStream["tlsSettings"] = map[string]any{} } case "none": if newStream["security"] != "none" { newStream["security"] = "none" delete(newStream, "tlsSettings") } } streamSettings, _ := json.MarshalIndent(newStream, "", " ") var newOutbounds []json_util.RawMessage switch inbound.Protocol { case "vmess": newOutbounds = append(newOutbounds, s.genVnext(inbound, streamSettings, client)) case "vless": newOutbounds = append(newOutbounds, s.genVless(inbound, streamSettings, client)) case "trojan", "shadowsocks": newOutbounds = append(newOutbounds, s.genServer(inbound, streamSettings, client)) } newOutbounds = append(newOutbounds, s.defaultOutbounds...) newConfigJson := make(map[string]any) maps.Copy(newConfigJson, s.configJson) newConfigJson["outbounds"] = newOutbounds newConfigJson["remarks"] = s.SubService.genRemark(inbound, client.Email, extPrxy["remark"].(string)) newConfig, _ := json.MarshalIndent(newConfigJson, "", " ") newJsonArray = append(newJsonArray, newConfig) } return newJsonArray } func (s *SubJsonService) streamData(stream string) map[string]any { var streamSettings map[string]any json.Unmarshal([]byte(stream), &streamSettings) security, _ := streamSettings["security"].(string) switch security { case "tls": streamSettings["tlsSettings"] = s.tlsData(streamSettings["tlsSettings"].(map[string]any)) case "reality": streamSettings["realitySettings"] = s.realityData(streamSettings["realitySettings"].(map[string]any)) } delete(streamSettings, "sockopt") if s.fragment != "" { streamSettings["sockopt"] = json_util.RawMessage(`{"dialerProxy": "fragment", "tcpKeepAliveIdle": 100, "tcpMptcp": true, "penetrate": true}`) } // remove proxy protocol network, _ := streamSettings["network"].(string) switch network { case "tcp": streamSettings["tcpSettings"] = s.removeAcceptProxy(streamSettings["tcpSettings"]) case "ws": streamSettings["wsSettings"] = s.removeAcceptProxy(streamSettings["wsSettings"]) case "httpupgrade": streamSettings["httpupgradeSettings"] = s.removeAcceptProxy(streamSettings["httpupgradeSettings"]) } return streamSettings } func (s *SubJsonService) removeAcceptProxy(setting any) map[string]any { netSettings, ok := setting.(map[string]any) if ok { delete(netSettings, "acceptProxyProtocol") } return netSettings } func (s *SubJsonService) tlsData(tData map[string]any) map[string]any { tlsData := make(map[string]any, 1) tlsClientSettings, _ := tData["settings"].(map[string]any) tlsData["serverName"] = tData["serverName"] tlsData["alpn"] = tData["alpn"] if fingerprint, ok := tlsClientSettings["fingerprint"].(string); ok { tlsData["fingerprint"] = fingerprint } return tlsData } func (s *SubJsonService) realityData(rData map[string]any) map[string]any { rltyData := make(map[string]any, 1) rltyClientSettings, _ := rData["settings"].(map[string]any) rltyData["show"] = false rltyData["publicKey"] = rltyClientSettings["publicKey"] rltyData["fingerprint"] = rltyClientSettings["fingerprint"] rltyData["mldsa65Verify"] = rltyClientSettings["mldsa65Verify"] // Set random data rltyData["spiderX"] = "/" + random.Seq(15) shortIds, ok := rData["shortIds"].([]any) if ok && len(shortIds) > 0 { rltyData["shortId"] = shortIds[random.Num(len(shortIds))].(string) } else { rltyData["shortId"] = "" } serverNames, ok := rData["serverNames"].([]any) if ok && len(serverNames) > 0 { rltyData["serverName"] = serverNames[random.Num(len(serverNames))].(string) } else { rltyData["serverName"] = "" } return rltyData } func (s *SubJsonService) genVnext(inbound *model.Inbound, streamSettings json_util.RawMessage, client model.Client) json_util.RawMessage { outbound := Outbound{} usersData := make([]UserVnext, 1) usersData[0].ID = client.ID usersData[0].Email = client.Email usersData[0].Security = client.Security vnextData := make([]VnextSetting, 1) vnextData[0] = VnextSetting{ Address: inbound.Listen, Port: inbound.Port, Users: usersData, } outbound.Protocol = string(inbound.Protocol) outbound.Tag = "proxy" if s.mux != "" { outbound.Mux = json_util.RawMessage(s.mux) } outbound.StreamSettings = streamSettings outbound.Settings = map[string]any{ "vnext": vnextData, } result, _ := json.MarshalIndent(outbound, "", " ") return result } func (s *SubJsonService) genVless(inbound *model.Inbound, streamSettings json_util.RawMessage, client model.Client) json_util.RawMessage { outbound := Outbound{} outbound.Protocol = string(inbound.Protocol) outbound.Tag = "proxy" if s.mux != "" { outbound.Mux = json_util.RawMessage(s.mux) } outbound.StreamSettings = streamSettings settings := make(map[string]any) settings["address"] = inbound.Listen settings["port"] = inbound.Port settings["id"] = client.ID if client.Flow != "" { settings["flow"] = client.Flow } // Add encryption for VLESS outbound from inbound settings var inboundSettings map[string]any json.Unmarshal([]byte(inbound.Settings), &inboundSettings) if encryption, ok := inboundSettings["encryption"].(string); ok { settings["encryption"] = encryption } outbound.Settings = settings result, _ := json.MarshalIndent(outbound, "", " ") return result } func (s *SubJsonService) genServer(inbound *model.Inbound, streamSettings json_util.RawMessage, client model.Client) json_util.RawMessage { outbound := Outbound{} serverData := make([]ServerSetting, 1) serverData[0] = ServerSetting{ Address: inbound.Listen, Port: inbound.Port, Level: 8, Password: client.Password, } if inbound.Protocol == model.Shadowsocks { var inboundSettings map[string]any json.Unmarshal([]byte(inbound.Settings), &inboundSettings) method, _ := inboundSettings["method"].(string) serverData[0].Method = method // server password in multi-user 2022 protocols if strings.HasPrefix(method, "2022") { if serverPassword, ok := inboundSettings["password"].(string); ok { serverData[0].Password = fmt.Sprintf("%s:%s", serverPassword, client.Password) } } } outbound.Protocol = string(inbound.Protocol) outbound.Tag = "proxy" if s.mux != "" { outbound.Mux = json_util.RawMessage(s.mux) } outbound.StreamSettings = streamSettings outbound.Settings = map[string]any{ "servers": serverData, } result, _ := json.MarshalIndent(outbound, "", " ") return result } type Outbound struct { Protocol string `json:"protocol"` Tag string `json:"tag"` StreamSettings json_util.RawMessage `json:"streamSettings"` Mux json_util.RawMessage `json:"mux,omitempty"` Settings map[string]any `json:"settings,omitempty"` } type VnextSetting struct { Address string `json:"address"` Port int `json:"port"` Users []UserVnext `json:"users"` } type UserVnext struct { ID string `json:"id"` Email string `json:"email,omitempty"` Security string `json:"security,omitempty"` } type ServerSetting struct { Password string `json:"password"` Level int `json:"level"` Address string `json:"address"` Port int `json:"port"` Flow string `json:"flow,omitempty"` Method string `json:"method,omitempty"` } ================================================ FILE: sub/subService.go ================================================ package sub import ( "encoding/base64" "fmt" "net" "net/url" "strings" "time" "github.com/gin-gonic/gin" "github.com/goccy/go-json" "github.com/mhsanaei/3x-ui/v2/database" "github.com/mhsanaei/3x-ui/v2/database/model" "github.com/mhsanaei/3x-ui/v2/logger" "github.com/mhsanaei/3x-ui/v2/util/common" "github.com/mhsanaei/3x-ui/v2/util/random" "github.com/mhsanaei/3x-ui/v2/web/service" "github.com/mhsanaei/3x-ui/v2/xray" ) // SubService provides business logic for generating subscription links and managing subscription data. type SubService struct { address string showInfo bool remarkModel string datepicker string inboundService service.InboundService settingService service.SettingService } // NewSubService creates a new subscription service with the given configuration. func NewSubService(showInfo bool, remarkModel string) *SubService { return &SubService{ showInfo: showInfo, remarkModel: remarkModel, } } // GetSubs retrieves subscription links for a given subscription ID and host. func (s *SubService) GetSubs(subId string, host string) ([]string, int64, xray.ClientTraffic, error) { s.address = host var result []string var traffic xray.ClientTraffic var lastOnline int64 var clientTraffics []xray.ClientTraffic inbounds, err := s.getInboundsBySubId(subId) if err != nil { return nil, 0, traffic, err } if len(inbounds) == 0 { return nil, 0, traffic, common.NewError("No inbounds found with ", subId) } s.datepicker, err = s.settingService.GetDatepicker() if err != nil { s.datepicker = "gregorian" } for _, inbound := range inbounds { clients, err := s.inboundService.GetClients(inbound) if err != nil { logger.Error("SubService - GetClients: Unable to get clients from inbound") } if clients == nil { continue } if len(inbound.Listen) > 0 && inbound.Listen[0] == '@' { listen, port, streamSettings, err := s.getFallbackMaster(inbound.Listen, inbound.StreamSettings) if err == nil { inbound.Listen = listen inbound.Port = port inbound.StreamSettings = streamSettings } } for _, client := range clients { if client.Enable && client.SubID == subId { link := s.getLink(inbound, client.Email) result = append(result, link) ct := s.getClientTraffics(inbound.ClientStats, client.Email) clientTraffics = append(clientTraffics, ct) if ct.LastOnline > lastOnline { lastOnline = ct.LastOnline } } } } // Prepare statistics for index, clientTraffic := range clientTraffics { if index == 0 { traffic.Up = clientTraffic.Up traffic.Down = clientTraffic.Down traffic.Total = clientTraffic.Total if clientTraffic.ExpiryTime > 0 { traffic.ExpiryTime = clientTraffic.ExpiryTime } } else { traffic.Up += clientTraffic.Up traffic.Down += clientTraffic.Down if traffic.Total == 0 || clientTraffic.Total == 0 { traffic.Total = 0 } else { traffic.Total += clientTraffic.Total } if clientTraffic.ExpiryTime != traffic.ExpiryTime { traffic.ExpiryTime = 0 } } } return result, lastOnline, traffic, nil } func (s *SubService) getInboundsBySubId(subId string) ([]*model.Inbound, error) { db := database.GetDB() var inbounds []*model.Inbound err := db.Model(model.Inbound{}).Preload("ClientStats").Where(`id in ( SELECT DISTINCT inbounds.id FROM inbounds, JSON_EACH(JSON_EXTRACT(inbounds.settings, '$.clients')) AS client WHERE protocol in ('vmess','vless','trojan','shadowsocks') AND JSON_EXTRACT(client.value, '$.subId') = ? AND enable = ? )`, subId, true).Find(&inbounds).Error if err != nil { return nil, err } return inbounds, nil } func (s *SubService) getClientTraffics(traffics []xray.ClientTraffic, email string) xray.ClientTraffic { for _, traffic := range traffics { if traffic.Email == email { return traffic } } return xray.ClientTraffic{} } func (s *SubService) getFallbackMaster(dest string, streamSettings string) (string, int, string, error) { db := database.GetDB() var inbound *model.Inbound err := db.Model(model.Inbound{}). Where("JSON_TYPE(settings, '$.fallbacks') = 'array'"). Where("EXISTS (SELECT * FROM json_each(settings, '$.fallbacks') WHERE json_extract(value, '$.dest') = ?)", dest). Find(&inbound).Error if err != nil { return "", 0, "", err } var stream map[string]any json.Unmarshal([]byte(streamSettings), &stream) var masterStream map[string]any json.Unmarshal([]byte(inbound.StreamSettings), &masterStream) stream["security"] = masterStream["security"] stream["tlsSettings"] = masterStream["tlsSettings"] stream["externalProxy"] = masterStream["externalProxy"] modifiedStream, _ := json.MarshalIndent(stream, "", " ") return inbound.Listen, inbound.Port, string(modifiedStream), nil } func (s *SubService) getLink(inbound *model.Inbound, email string) string { switch inbound.Protocol { case "vmess": return s.genVmessLink(inbound, email) case "vless": return s.genVlessLink(inbound, email) case "trojan": return s.genTrojanLink(inbound, email) case "shadowsocks": return s.genShadowsocksLink(inbound, email) } return "" } func (s *SubService) genVmessLink(inbound *model.Inbound, email string) string { if inbound.Protocol != model.VMESS { return "" } var address string if inbound.Listen == "" || inbound.Listen == "0.0.0.0" || inbound.Listen == "::" || inbound.Listen == "::0" { address = s.address } else { address = inbound.Listen } obj := map[string]any{ "v": "2", "add": address, "port": inbound.Port, "type": "none", } var stream map[string]any json.Unmarshal([]byte(inbound.StreamSettings), &stream) network, _ := stream["network"].(string) obj["net"] = network switch network { case "tcp": tcp, _ := stream["tcpSettings"].(map[string]any) header, _ := tcp["header"].(map[string]any) typeStr, _ := header["type"].(string) obj["type"] = typeStr if typeStr == "http" { request := header["request"].(map[string]any) requestPath, _ := request["path"].([]any) obj["path"] = requestPath[0].(string) headers, _ := request["headers"].(map[string]any) obj["host"] = searchHost(headers) } case "kcp": kcp, _ := stream["kcpSettings"].(map[string]any) header, _ := kcp["header"].(map[string]any) obj["type"], _ = header["type"].(string) obj["path"], _ = kcp["seed"].(string) case "ws": ws, _ := stream["wsSettings"].(map[string]any) obj["path"] = ws["path"].(string) if host, ok := ws["host"].(string); ok && len(host) > 0 { obj["host"] = host } else { headers, _ := ws["headers"].(map[string]any) obj["host"] = searchHost(headers) } case "grpc": grpc, _ := stream["grpcSettings"].(map[string]any) obj["path"] = grpc["serviceName"].(string) obj["authority"] = grpc["authority"].(string) if grpc["multiMode"].(bool) { obj["type"] = "multi" } case "httpupgrade": httpupgrade, _ := stream["httpupgradeSettings"].(map[string]any) obj["path"] = httpupgrade["path"].(string) if host, ok := httpupgrade["host"].(string); ok && len(host) > 0 { obj["host"] = host } else { headers, _ := httpupgrade["headers"].(map[string]any) obj["host"] = searchHost(headers) } case "xhttp": xhttp, _ := stream["xhttpSettings"].(map[string]any) obj["path"] = xhttp["path"].(string) if host, ok := xhttp["host"].(string); ok && len(host) > 0 { obj["host"] = host } else { headers, _ := xhttp["headers"].(map[string]any) obj["host"] = searchHost(headers) } obj["mode"] = xhttp["mode"].(string) } security, _ := stream["security"].(string) obj["tls"] = security if security == "tls" { tlsSetting, _ := stream["tlsSettings"].(map[string]any) alpns, _ := tlsSetting["alpn"].([]any) if len(alpns) > 0 { var alpn []string for _, a := range alpns { alpn = append(alpn, a.(string)) } obj["alpn"] = strings.Join(alpn, ",") } if sniValue, ok := searchKey(tlsSetting, "serverName"); ok { obj["sni"], _ = sniValue.(string) } tlsSettings, _ := searchKey(tlsSetting, "settings") if tlsSetting != nil { if fpValue, ok := searchKey(tlsSettings, "fingerprint"); ok { obj["fp"], _ = fpValue.(string) } } } clients, _ := s.inboundService.GetClients(inbound) clientIndex := -1 for i, client := range clients { if client.Email == email { clientIndex = i break } } obj["id"] = clients[clientIndex].ID obj["scy"] = clients[clientIndex].Security externalProxies, _ := stream["externalProxy"].([]any) if len(externalProxies) > 0 { links := "" for index, externalProxy := range externalProxies { ep, _ := externalProxy.(map[string]any) newSecurity, _ := ep["forceTls"].(string) newObj := map[string]any{} for key, value := range obj { if !(newSecurity == "none" && (key == "alpn" || key == "sni" || key == "fp")) { newObj[key] = value } } newObj["ps"] = s.genRemark(inbound, email, ep["remark"].(string)) newObj["add"] = ep["dest"].(string) newObj["port"] = int(ep["port"].(float64)) if newSecurity != "same" { newObj["tls"] = newSecurity } if index > 0 { links += "\n" } jsonStr, _ := json.MarshalIndent(newObj, "", " ") links += "vmess://" + base64.StdEncoding.EncodeToString(jsonStr) } return links } obj["ps"] = s.genRemark(inbound, email, "") jsonStr, _ := json.MarshalIndent(obj, "", " ") return "vmess://" + base64.StdEncoding.EncodeToString(jsonStr) } func (s *SubService) genVlessLink(inbound *model.Inbound, email string) string { var address string if inbound.Listen == "" || inbound.Listen == "0.0.0.0" || inbound.Listen == "::" || inbound.Listen == "::0" { address = s.address } else { address = inbound.Listen } if inbound.Protocol != model.VLESS { return "" } var stream map[string]any json.Unmarshal([]byte(inbound.StreamSettings), &stream) clients, _ := s.inboundService.GetClients(inbound) clientIndex := -1 for i, client := range clients { if client.Email == email { clientIndex = i break } } uuid := clients[clientIndex].ID port := inbound.Port streamNetwork := stream["network"].(string) params := make(map[string]string) params["type"] = streamNetwork // Add encryption parameter for VLESS from inbound settings var settings map[string]any json.Unmarshal([]byte(inbound.Settings), &settings) if encryption, ok := settings["encryption"].(string); ok { params["encryption"] = encryption } switch streamNetwork { case "tcp": tcp, _ := stream["tcpSettings"].(map[string]any) header, _ := tcp["header"].(map[string]any) typeStr, _ := header["type"].(string) if typeStr == "http" { request := header["request"].(map[string]any) requestPath, _ := request["path"].([]any) params["path"] = requestPath[0].(string) headers, _ := request["headers"].(map[string]any) params["host"] = searchHost(headers) params["headerType"] = "http" } case "kcp": kcp, _ := stream["kcpSettings"].(map[string]any) header, _ := kcp["header"].(map[string]any) params["headerType"] = header["type"].(string) params["seed"] = kcp["seed"].(string) case "ws": ws, _ := stream["wsSettings"].(map[string]any) params["path"] = ws["path"].(string) if host, ok := ws["host"].(string); ok && len(host) > 0 { params["host"] = host } else { headers, _ := ws["headers"].(map[string]any) params["host"] = searchHost(headers) } case "grpc": grpc, _ := stream["grpcSettings"].(map[string]any) params["serviceName"] = grpc["serviceName"].(string) params["authority"], _ = grpc["authority"].(string) if grpc["multiMode"].(bool) { params["mode"] = "multi" } case "httpupgrade": httpupgrade, _ := stream["httpupgradeSettings"].(map[string]any) params["path"] = httpupgrade["path"].(string) if host, ok := httpupgrade["host"].(string); ok && len(host) > 0 { params["host"] = host } else { headers, _ := httpupgrade["headers"].(map[string]any) params["host"] = searchHost(headers) } case "xhttp": xhttp, _ := stream["xhttpSettings"].(map[string]any) params["path"] = xhttp["path"].(string) if host, ok := xhttp["host"].(string); ok && len(host) > 0 { params["host"] = host } else { headers, _ := xhttp["headers"].(map[string]any) params["host"] = searchHost(headers) } params["mode"] = xhttp["mode"].(string) } security, _ := stream["security"].(string) if security == "tls" { params["security"] = "tls" tlsSetting, _ := stream["tlsSettings"].(map[string]any) alpns, _ := tlsSetting["alpn"].([]any) var alpn []string for _, a := range alpns { alpn = append(alpn, a.(string)) } if len(alpn) > 0 { params["alpn"] = strings.Join(alpn, ",") } if sniValue, ok := searchKey(tlsSetting, "serverName"); ok { params["sni"], _ = sniValue.(string) } tlsSettings, _ := searchKey(tlsSetting, "settings") if tlsSetting != nil { if fpValue, ok := searchKey(tlsSettings, "fingerprint"); ok { params["fp"], _ = fpValue.(string) } } if streamNetwork == "tcp" && len(clients[clientIndex].Flow) > 0 { params["flow"] = clients[clientIndex].Flow } } if security == "reality" { params["security"] = "reality" realitySetting, _ := stream["realitySettings"].(map[string]any) realitySettings, _ := searchKey(realitySetting, "settings") if realitySetting != nil { if sniValue, ok := searchKey(realitySetting, "serverNames"); ok { sNames, _ := sniValue.([]any) params["sni"] = sNames[random.Num(len(sNames))].(string) } if pbkValue, ok := searchKey(realitySettings, "publicKey"); ok { params["pbk"], _ = pbkValue.(string) } if sidValue, ok := searchKey(realitySetting, "shortIds"); ok { shortIds, _ := sidValue.([]any) params["sid"] = shortIds[random.Num(len(shortIds))].(string) } if fpValue, ok := searchKey(realitySettings, "fingerprint"); ok { if fp, ok := fpValue.(string); ok && len(fp) > 0 { params["fp"] = fp } } if pqvValue, ok := searchKey(realitySettings, "mldsa65Verify"); ok { if pqv, ok := pqvValue.(string); ok && len(pqv) > 0 { params["pqv"] = pqv } } params["spx"] = "/" + random.Seq(15) } if streamNetwork == "tcp" && len(clients[clientIndex].Flow) > 0 { params["flow"] = clients[clientIndex].Flow } } if security != "tls" && security != "reality" { params["security"] = "none" } externalProxies, _ := stream["externalProxy"].([]any) if len(externalProxies) > 0 { links := make([]string, 0, len(externalProxies)) for _, externalProxy := range externalProxies { ep, _ := externalProxy.(map[string]any) newSecurity, _ := ep["forceTls"].(string) dest, _ := ep["dest"].(string) port := int(ep["port"].(float64)) link := fmt.Sprintf("vless://%s@%s:%d", uuid, dest, port) if newSecurity != "same" { params["security"] = newSecurity } else { params["security"] = security } url, _ := url.Parse(link) q := url.Query() for k, v := range params { if !(newSecurity == "none" && (k == "alpn" || k == "sni" || k == "fp")) { q.Add(k, v) } } // Set the new query values on the URL url.RawQuery = q.Encode() url.Fragment = s.genRemark(inbound, email, ep["remark"].(string)) links = append(links, url.String()) } return strings.Join(links, "\n") } link := fmt.Sprintf("vless://%s@%s:%d", uuid, address, port) url, _ := url.Parse(link) q := url.Query() for k, v := range params { q.Add(k, v) } // Set the new query values on the URL url.RawQuery = q.Encode() url.Fragment = s.genRemark(inbound, email, "") return url.String() } func (s *SubService) genTrojanLink(inbound *model.Inbound, email string) string { var address string if inbound.Listen == "" || inbound.Listen == "0.0.0.0" || inbound.Listen == "::" || inbound.Listen == "::0" { address = s.address } else { address = inbound.Listen } if inbound.Protocol != model.Trojan { return "" } var stream map[string]any json.Unmarshal([]byte(inbound.StreamSettings), &stream) clients, _ := s.inboundService.GetClients(inbound) clientIndex := -1 for i, client := range clients { if client.Email == email { clientIndex = i break } } password := clients[clientIndex].Password port := inbound.Port streamNetwork := stream["network"].(string) params := make(map[string]string) params["type"] = streamNetwork switch streamNetwork { case "tcp": tcp, _ := stream["tcpSettings"].(map[string]any) header, _ := tcp["header"].(map[string]any) typeStr, _ := header["type"].(string) if typeStr == "http" { request := header["request"].(map[string]any) requestPath, _ := request["path"].([]any) params["path"] = requestPath[0].(string) headers, _ := request["headers"].(map[string]any) params["host"] = searchHost(headers) params["headerType"] = "http" } case "kcp": kcp, _ := stream["kcpSettings"].(map[string]any) header, _ := kcp["header"].(map[string]any) params["headerType"] = header["type"].(string) params["seed"] = kcp["seed"].(string) case "ws": ws, _ := stream["wsSettings"].(map[string]any) params["path"] = ws["path"].(string) if host, ok := ws["host"].(string); ok && len(host) > 0 { params["host"] = host } else { headers, _ := ws["headers"].(map[string]any) params["host"] = searchHost(headers) } case "grpc": grpc, _ := stream["grpcSettings"].(map[string]any) params["serviceName"] = grpc["serviceName"].(string) params["authority"], _ = grpc["authority"].(string) if grpc["multiMode"].(bool) { params["mode"] = "multi" } case "httpupgrade": httpupgrade, _ := stream["httpupgradeSettings"].(map[string]any) params["path"] = httpupgrade["path"].(string) if host, ok := httpupgrade["host"].(string); ok && len(host) > 0 { params["host"] = host } else { headers, _ := httpupgrade["headers"].(map[string]any) params["host"] = searchHost(headers) } case "xhttp": xhttp, _ := stream["xhttpSettings"].(map[string]any) params["path"] = xhttp["path"].(string) if host, ok := xhttp["host"].(string); ok && len(host) > 0 { params["host"] = host } else { headers, _ := xhttp["headers"].(map[string]any) params["host"] = searchHost(headers) } params["mode"] = xhttp["mode"].(string) } security, _ := stream["security"].(string) if security == "tls" { params["security"] = "tls" tlsSetting, _ := stream["tlsSettings"].(map[string]any) alpns, _ := tlsSetting["alpn"].([]any) var alpn []string for _, a := range alpns { alpn = append(alpn, a.(string)) } if len(alpn) > 0 { params["alpn"] = strings.Join(alpn, ",") } if sniValue, ok := searchKey(tlsSetting, "serverName"); ok { params["sni"], _ = sniValue.(string) } tlsSettings, _ := searchKey(tlsSetting, "settings") if tlsSetting != nil { if fpValue, ok := searchKey(tlsSettings, "fingerprint"); ok { params["fp"], _ = fpValue.(string) } } } if security == "reality" { params["security"] = "reality" realitySetting, _ := stream["realitySettings"].(map[string]any) realitySettings, _ := searchKey(realitySetting, "settings") if realitySetting != nil { if sniValue, ok := searchKey(realitySetting, "serverNames"); ok { sNames, _ := sniValue.([]any) params["sni"] = sNames[random.Num(len(sNames))].(string) } if pbkValue, ok := searchKey(realitySettings, "publicKey"); ok { params["pbk"], _ = pbkValue.(string) } if sidValue, ok := searchKey(realitySetting, "shortIds"); ok { shortIds, _ := sidValue.([]any) params["sid"] = shortIds[random.Num(len(shortIds))].(string) } if fpValue, ok := searchKey(realitySettings, "fingerprint"); ok { if fp, ok := fpValue.(string); ok && len(fp) > 0 { params["fp"] = fp } } if pqvValue, ok := searchKey(realitySettings, "mldsa65Verify"); ok { if pqv, ok := pqvValue.(string); ok && len(pqv) > 0 { params["pqv"] = pqv } } params["spx"] = "/" + random.Seq(15) } if streamNetwork == "tcp" && len(clients[clientIndex].Flow) > 0 { params["flow"] = clients[clientIndex].Flow } } if security != "tls" && security != "reality" { params["security"] = "none" } externalProxies, _ := stream["externalProxy"].([]any) if len(externalProxies) > 0 { links := "" for index, externalProxy := range externalProxies { ep, _ := externalProxy.(map[string]any) newSecurity, _ := ep["forceTls"].(string) dest, _ := ep["dest"].(string) port := int(ep["port"].(float64)) link := fmt.Sprintf("trojan://%s@%s:%d", password, dest, port) if newSecurity != "same" { params["security"] = newSecurity } else { params["security"] = security } url, _ := url.Parse(link) q := url.Query() for k, v := range params { if !(newSecurity == "none" && (k == "alpn" || k == "sni" || k == "fp")) { q.Add(k, v) } } // Set the new query values on the URL url.RawQuery = q.Encode() url.Fragment = s.genRemark(inbound, email, ep["remark"].(string)) if index > 0 { links += "\n" } links += url.String() } return links } link := fmt.Sprintf("trojan://%s@%s:%d", password, address, port) url, _ := url.Parse(link) q := url.Query() for k, v := range params { q.Add(k, v) } // Set the new query values on the URL url.RawQuery = q.Encode() url.Fragment = s.genRemark(inbound, email, "") return url.String() } func (s *SubService) genShadowsocksLink(inbound *model.Inbound, email string) string { var address string if inbound.Listen == "" || inbound.Listen == "0.0.0.0" || inbound.Listen == "::" || inbound.Listen == "::0" { address = s.address } else { address = inbound.Listen } if inbound.Protocol != model.Shadowsocks { return "" } var stream map[string]any json.Unmarshal([]byte(inbound.StreamSettings), &stream) clients, _ := s.inboundService.GetClients(inbound) var settings map[string]any json.Unmarshal([]byte(inbound.Settings), &settings) inboundPassword := settings["password"].(string) method := settings["method"].(string) clientIndex := -1 for i, client := range clients { if client.Email == email { clientIndex = i break } } streamNetwork := stream["network"].(string) params := make(map[string]string) params["type"] = streamNetwork switch streamNetwork { case "tcp": tcp, _ := stream["tcpSettings"].(map[string]any) header, _ := tcp["header"].(map[string]any) typeStr, _ := header["type"].(string) if typeStr == "http" { request := header["request"].(map[string]any) requestPath, _ := request["path"].([]any) params["path"] = requestPath[0].(string) headers, _ := request["headers"].(map[string]any) params["host"] = searchHost(headers) params["headerType"] = "http" } case "kcp": kcp, _ := stream["kcpSettings"].(map[string]any) header, _ := kcp["header"].(map[string]any) params["headerType"] = header["type"].(string) params["seed"] = kcp["seed"].(string) case "ws": ws, _ := stream["wsSettings"].(map[string]any) params["path"] = ws["path"].(string) if host, ok := ws["host"].(string); ok && len(host) > 0 { params["host"] = host } else { headers, _ := ws["headers"].(map[string]any) params["host"] = searchHost(headers) } case "grpc": grpc, _ := stream["grpcSettings"].(map[string]any) params["serviceName"] = grpc["serviceName"].(string) params["authority"], _ = grpc["authority"].(string) if grpc["multiMode"].(bool) { params["mode"] = "multi" } case "httpupgrade": httpupgrade, _ := stream["httpupgradeSettings"].(map[string]any) params["path"] = httpupgrade["path"].(string) if host, ok := httpupgrade["host"].(string); ok && len(host) > 0 { params["host"] = host } else { headers, _ := httpupgrade["headers"].(map[string]any) params["host"] = searchHost(headers) } case "xhttp": xhttp, _ := stream["xhttpSettings"].(map[string]any) params["path"] = xhttp["path"].(string) if host, ok := xhttp["host"].(string); ok && len(host) > 0 { params["host"] = host } else { headers, _ := xhttp["headers"].(map[string]any) params["host"] = searchHost(headers) } params["mode"] = xhttp["mode"].(string) } security, _ := stream["security"].(string) if security == "tls" { params["security"] = "tls" tlsSetting, _ := stream["tlsSettings"].(map[string]any) alpns, _ := tlsSetting["alpn"].([]any) var alpn []string for _, a := range alpns { alpn = append(alpn, a.(string)) } if len(alpn) > 0 { params["alpn"] = strings.Join(alpn, ",") } if sniValue, ok := searchKey(tlsSetting, "serverName"); ok { params["sni"], _ = sniValue.(string) } tlsSettings, _ := searchKey(tlsSetting, "settings") if tlsSetting != nil { if fpValue, ok := searchKey(tlsSettings, "fingerprint"); ok { params["fp"], _ = fpValue.(string) } } } encPart := fmt.Sprintf("%s:%s", method, clients[clientIndex].Password) if method[0] == '2' { encPart = fmt.Sprintf("%s:%s:%s", method, inboundPassword, clients[clientIndex].Password) } externalProxies, _ := stream["externalProxy"].([]any) if len(externalProxies) > 0 { links := "" for index, externalProxy := range externalProxies { ep, _ := externalProxy.(map[string]any) newSecurity, _ := ep["forceTls"].(string) dest, _ := ep["dest"].(string) port := int(ep["port"].(float64)) link := fmt.Sprintf("ss://%s@%s:%d", base64.StdEncoding.EncodeToString([]byte(encPart)), dest, port) if newSecurity != "same" { params["security"] = newSecurity } else { params["security"] = security } url, _ := url.Parse(link) q := url.Query() for k, v := range params { if !(newSecurity == "none" && (k == "alpn" || k == "sni" || k == "fp")) { q.Add(k, v) } } // Set the new query values on the URL url.RawQuery = q.Encode() url.Fragment = s.genRemark(inbound, email, ep["remark"].(string)) if index > 0 { links += "\n" } links += url.String() } return links } link := fmt.Sprintf("ss://%s@%s:%d", base64.StdEncoding.EncodeToString([]byte(encPart)), address, inbound.Port) url, _ := url.Parse(link) q := url.Query() for k, v := range params { q.Add(k, v) } // Set the new query values on the URL url.RawQuery = q.Encode() url.Fragment = s.genRemark(inbound, email, "") return url.String() } func (s *SubService) genRemark(inbound *model.Inbound, email string, extra string) string { separationChar := string(s.remarkModel[0]) orderChars := s.remarkModel[1:] orders := map[byte]string{ 'i': "", 'e': "", 'o': "", } if len(email) > 0 { orders['e'] = email } if len(inbound.Remark) > 0 { orders['i'] = inbound.Remark } if len(extra) > 0 { orders['o'] = extra } var remark []string for i := 0; i < len(orderChars); i++ { char := orderChars[i] order, exists := orders[char] if exists && order != "" { remark = append(remark, order) } } if s.showInfo { statsExist := false var stats xray.ClientTraffic for _, clientStat := range inbound.ClientStats { if clientStat.Email == email { stats = clientStat statsExist = true break } } // Get remained days if statsExist { if !stats.Enable { return fmt.Sprintf("⛔️N/A%s%s", separationChar, strings.Join(remark, separationChar)) } if vol := stats.Total - (stats.Up + stats.Down); vol > 0 { remark = append(remark, fmt.Sprintf("%s%s", common.FormatTraffic(vol), "📊")) } now := time.Now().Unix() switch exp := stats.ExpiryTime / 1000; { case exp > 0: remainingSeconds := exp - now days := remainingSeconds / 86400 hours := (remainingSeconds % 86400) / 3600 minutes := (remainingSeconds % 3600) / 60 if days > 0 { if hours > 0 { remark = append(remark, fmt.Sprintf("%dD,%dH⏳", days, hours)) } else { remark = append(remark, fmt.Sprintf("%dD⏳", days)) } } else if hours > 0 { remark = append(remark, fmt.Sprintf("%dH⏳", hours)) } else { remark = append(remark, fmt.Sprintf("%dM⏳", minutes)) } case exp < 0: days := exp / -86400 hours := (exp % -86400) / 3600 minutes := (exp % -3600) / 60 if days > 0 { if hours > 0 { remark = append(remark, fmt.Sprintf("%dD,%dH⏳", days, hours)) } else { remark = append(remark, fmt.Sprintf("%dD⏳", days)) } } else if hours > 0 { remark = append(remark, fmt.Sprintf("%dH⏳", hours)) } else { remark = append(remark, fmt.Sprintf("%dM⏳", minutes)) } } } } return strings.Join(remark, separationChar) } func searchKey(data any, key string) (any, bool) { switch val := data.(type) { case map[string]any: for k, v := range val { if k == key { return v, true } if result, ok := searchKey(v, key); ok { return result, true } } case []any: for _, v := range val { if result, ok := searchKey(v, key); ok { return result, true } } } return nil, false } func searchHost(headers any) string { data, _ := headers.(map[string]any) for k, v := range data { if strings.EqualFold(k, "host") { switch v.(type) { case []any: hosts, _ := v.([]any) if len(hosts) > 0 { return hosts[0].(string) } else { return "" } case any: return v.(string) } } } return "" } // PageData is a view model for subpage.html // PageData contains data for rendering the subscription information page. type PageData struct { Host string BasePath string SId string Download string Upload string Total string Used string Remained string Expire int64 LastOnline int64 Datepicker string DownloadByte int64 UploadByte int64 TotalByte int64 SubUrl string SubJsonUrl string Result []string } // ResolveRequest extracts scheme and host info from request/headers consistently. // ResolveRequest extracts scheme, host, and header information from an HTTP request. func (s *SubService) ResolveRequest(c *gin.Context) (scheme string, host string, hostWithPort string, hostHeader string) { // scheme scheme = "http" if c.Request.TLS != nil || strings.EqualFold(c.GetHeader("X-Forwarded-Proto"), "https") { scheme = "https" } // base host (no port) if h, err := getHostFromXFH(c.GetHeader("X-Forwarded-Host")); err == nil && h != "" { host = h } if host == "" { host = c.GetHeader("X-Real-IP") } if host == "" { var err error host, _, err = net.SplitHostPort(c.Request.Host) if err != nil { host = c.Request.Host } } // host:port for URLs hostWithPort = c.GetHeader("X-Forwarded-Host") if hostWithPort == "" { hostWithPort = c.Request.Host } if hostWithPort == "" { hostWithPort = host } // header display host hostHeader = c.GetHeader("X-Forwarded-Host") if hostHeader == "" { hostHeader = c.GetHeader("X-Real-IP") } if hostHeader == "" { hostHeader = host } return } // BuildURLs constructs absolute subscription and JSON subscription URLs for a given subscription ID. // It prioritizes configured URIs, then individual settings, and finally falls back to request-derived components. func (s *SubService) BuildURLs(scheme, hostWithPort, subPath, subJsonPath, subId string) (subURL, subJsonURL string) { // Input validation if subId == "" { return "", "" } // Get configured URIs first (highest priority) configuredSubURI, _ := s.settingService.GetSubURI() configuredSubJsonURI, _ := s.settingService.GetSubJsonURI() // Determine base scheme and host (cached to avoid duplicate calls) var baseScheme, baseHostWithPort string if configuredSubURI == "" || configuredSubJsonURI == "" { baseScheme, baseHostWithPort = s.getBaseSchemeAndHost(scheme, hostWithPort) } // Build subscription URL subURL = s.buildSingleURL(configuredSubURI, baseScheme, baseHostWithPort, subPath, subId) // Build JSON subscription URL subJsonURL = s.buildSingleURL(configuredSubJsonURI, baseScheme, baseHostWithPort, subJsonPath, subId) return subURL, subJsonURL } // getBaseSchemeAndHost determines the base scheme and host from settings or falls back to request values func (s *SubService) getBaseSchemeAndHost(requestScheme, requestHostWithPort string) (string, string) { subDomain, err := s.settingService.GetSubDomain() if err != nil || subDomain == "" { return requestScheme, requestHostWithPort } // Get port and TLS settings subPort, _ := s.settingService.GetSubPort() subKeyFile, _ := s.settingService.GetSubKeyFile() subCertFile, _ := s.settingService.GetSubCertFile() // Determine scheme from TLS configuration scheme := "http" if subKeyFile != "" && subCertFile != "" { scheme = "https" } // Build host:port, always include port for clarity hostWithPort := fmt.Sprintf("%s:%d", subDomain, subPort) return scheme, hostWithPort } // buildSingleURL constructs a single URL using configured URI or base components func (s *SubService) buildSingleURL(configuredURI, baseScheme, baseHostWithPort, basePath, subId string) string { if configuredURI != "" { return s.joinPathWithID(configuredURI, subId) } baseURL := fmt.Sprintf("%s://%s", baseScheme, baseHostWithPort) return s.joinPathWithID(baseURL+basePath, subId) } // joinPathWithID safely joins a base path with a subscription ID func (s *SubService) joinPathWithID(basePath, subId string) string { if strings.HasSuffix(basePath, "/") { return basePath + subId } return basePath + "/" + subId } // BuildPageData parses header and prepares the template view model. // BuildPageData constructs page data for rendering the subscription information page. func (s *SubService) BuildPageData(subId string, hostHeader string, traffic xray.ClientTraffic, lastOnline int64, subs []string, subURL, subJsonURL string, basePath string) PageData { download := common.FormatTraffic(traffic.Down) upload := common.FormatTraffic(traffic.Up) total := "∞" used := common.FormatTraffic(traffic.Up + traffic.Down) remained := "" if traffic.Total > 0 { total = common.FormatTraffic(traffic.Total) left := max(traffic.Total-(traffic.Up+traffic.Down), 0) remained = common.FormatTraffic(left) } datepicker := s.datepicker if datepicker == "" { datepicker = "gregorian" } return PageData{ Host: hostHeader, BasePath: basePath, SId: subId, Download: download, Upload: upload, Total: total, Used: used, Remained: remained, Expire: traffic.ExpiryTime / 1000, LastOnline: lastOnline, Datepicker: datepicker, DownloadByte: traffic.Down, UploadByte: traffic.Up, TotalByte: traffic.Total, SubUrl: subURL, SubJsonUrl: subJsonURL, Result: subs, } } func getHostFromXFH(s string) (string, error) { if strings.Contains(s, ":") { realHost, _, err := net.SplitHostPort(s) if err != nil { return "", err } return realHost, nil } return s, nil } ================================================ FILE: update.sh ================================================ #!/bin/bash red='\033[0;31m' green='\033[0;32m' blue='\033[0;34m' yellow='\033[0;33m' plain='\033[0m' xui_folder="${XUI_MAIN_FOLDER:=/usr/local/x-ui}" xui_service="${XUI_SERVICE:=/etc/systemd/system}" # Don't edit this config b_source="${BASH_SOURCE[0]}" while [ -h "$b_source" ]; do b_dir="$(cd -P "$(dirname "$b_source")" >/dev/null 2>&1 && pwd || pwd -P)" b_source="$(readlink "$b_source")" [[ $b_source != /* ]] && b_source="$b_dir/$b_source" done cur_dir="$(cd -P "$(dirname "$b_source")" >/dev/null 2>&1 && pwd || pwd -P)" script_name=$(basename "$0") # Check command exist function _command_exists() { type "$1" &>/dev/null } # Fail, log and exit script function _fail() { local msg=${1} echo -e "${red}${msg}${plain}" exit 2 } # check root [[ $EUID -ne 0 ]] && _fail "FATAL ERROR: Please run this script with root privilege." if _command_exists curl; then curl_bin=$(which curl) else _fail "ERROR: Command 'curl' not found." fi # Check OS and set release variable if [[ -f /etc/os-release ]]; then source /etc/os-release release=$ID elif [[ -f /usr/lib/os-release ]]; then source /usr/lib/os-release release=$ID else _fail "Failed to check the system OS, please contact the author!" fi echo "The OS release is: $release" arch() { case "$(uname -m)" in x86_64 | x64 | amd64) echo 'amd64' ;; i*86 | x86) echo '386' ;; armv8* | armv8 | arm64 | aarch64) echo 'arm64' ;; armv7* | armv7 | arm) echo 'armv7' ;; armv6* | armv6) echo 'armv6' ;; armv5* | armv5) echo 'armv5' ;; s390x) echo 's390x' ;; *) echo -e "${red}Unsupported CPU architecture!${plain}" && rm -f "${cur_dir}/${script_name}" >/dev/null 2>&1 && exit 2;; esac } echo "Arch: $(arch)" # Simple helpers is_ipv4() { [[ "$1" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]] && return 0 || return 1 } is_ipv6() { [[ "$1" =~ : ]] && return 0 || return 1 } is_ip() { is_ipv4 "$1" || is_ipv6 "$1" } is_domain() { [[ "$1" =~ ^([A-Za-z0-9](-*[A-Za-z0-9])*\.)+(xn--[a-z0-9]{2,}|[A-Za-z]{2,})$ ]] && return 0 || return 1 } # Port helpers is_port_in_use() { local port="$1" if command -v ss >/dev/null 2>&1; then ss -ltn 2>/dev/null | awk -v p=":${port}$" '$4 ~ p {exit 0} END {exit 1}' return fi if command -v netstat >/dev/null 2>&1; then netstat -lnt 2>/dev/null | awk -v p=":${port} " '$4 ~ p {exit 0} END {exit 1}' return fi if command -v lsof >/dev/null 2>&1; then lsof -nP -iTCP:${port} -sTCP:LISTEN >/dev/null 2>&1 && return 0 fi return 1 } gen_random_string() { local length="$1" local random_string=$(LC_ALL=C tr -dc 'a-zA-Z0-9' /dev/null 2>&1 && apt-get install -y -q curl tar tzdata socat >/dev/null 2>&1 ;; fedora | amzn | virtuozzo | rhel | almalinux | rocky | ol) dnf -y update >/dev/null 2>&1 && dnf install -y -q curl tar tzdata socat >/dev/null 2>&1 ;; centos) if [[ "${VERSION_ID}" =~ ^7 ]]; then yum -y update >/dev/null 2>&1 && yum install -y -q curl tar tzdata socat >/dev/null 2>&1 else dnf -y update >/dev/null 2>&1 && dnf install -y -q curl tar tzdata socat >/dev/null 2>&1 fi ;; arch | manjaro | parch) pacman -Syu >/dev/null 2>&1 && pacman -Syu --noconfirm curl tar tzdata socat >/dev/null 2>&1 ;; opensuse-tumbleweed | opensuse-leap) zypper refresh >/dev/null 2>&1 && zypper -q install -y curl tar timezone socat >/dev/null 2>&1 ;; alpine) apk update >/dev/null 2>&1 && apk add curl tar tzdata socat >/dev/null 2>&1 ;; *) apt-get update >/dev/null 2>&1 && apt install -y -q curl tar tzdata socat >/dev/null 2>&1 ;; esac } install_acme() { echo -e "${green}Installing acme.sh for SSL certificate management...${plain}" cd ~ || return 1 curl -s https://get.acme.sh | sh >/dev/null 2>&1 if [ $? -ne 0 ]; then echo -e "${red}Failed to install acme.sh${plain}" return 1 else echo -e "${green}acme.sh installed successfully${plain}" fi return 0 } setup_ssl_certificate() { local domain="$1" local server_ip="$2" local existing_port="$3" local existing_webBasePath="$4" echo -e "${green}Setting up SSL certificate...${plain}" # Check if acme.sh is installed if ! command -v ~/.acme.sh/acme.sh &>/dev/null; then install_acme if [ $? -ne 0 ]; then echo -e "${yellow}Failed to install acme.sh, skipping SSL setup${plain}" return 1 fi fi # Create certificate directory local certPath="/root/cert/${domain}" mkdir -p "$certPath" # Issue certificate echo -e "${green}Issuing SSL certificate for ${domain}...${plain}" echo -e "${yellow}Note: Port 80 must be open and accessible from the internet${plain}" ~/.acme.sh/acme.sh --set-default-ca --server letsencrypt --force >/dev/null 2>&1 ~/.acme.sh/acme.sh --issue -d ${domain} --listen-v6 --standalone --httpport 80 --force if [ $? -ne 0 ]; then echo -e "${yellow}Failed to issue certificate for ${domain}${plain}" echo -e "${yellow}Please ensure port 80 is open and try again later with: x-ui${plain}" rm -rf ~/.acme.sh/${domain} 2>/dev/null rm -rf "$certPath" 2>/dev/null return 1 fi # Install certificate ~/.acme.sh/acme.sh --installcert -d ${domain} \ --key-file /root/cert/${domain}/privkey.pem \ --fullchain-file /root/cert/${domain}/fullchain.pem \ --reloadcmd "systemctl restart x-ui" >/dev/null 2>&1 if [ $? -ne 0 ]; then echo -e "${yellow}Failed to install certificate${plain}" return 1 fi # Enable auto-renew ~/.acme.sh/acme.sh --upgrade --auto-upgrade >/dev/null 2>&1 chmod 600 $certPath/privkey.pem 2>/dev/null chmod 644 $certPath/fullchain.pem 2>/dev/null # Set certificate for panel local webCertFile="/root/cert/${domain}/fullchain.pem" local webKeyFile="/root/cert/${domain}/privkey.pem" if [[ -f "$webCertFile" && -f "$webKeyFile" ]]; then ${xui_folder}/x-ui cert -webCert "$webCertFile" -webCertKey "$webKeyFile" >/dev/null 2>&1 echo -e "${green}SSL certificate installed and configured successfully!${plain}" return 0 else echo -e "${yellow}Certificate files not found${plain}" return 1 fi } # Issue Let's Encrypt IP certificate with shortlived profile (~6 days validity) # Requires acme.sh and port 80 open for HTTP-01 challenge setup_ip_certificate() { local ipv4="$1" local ipv6="$2" # optional echo -e "${green}Setting up Let's Encrypt IP certificate (shortlived profile)...${plain}" echo -e "${yellow}Note: IP certificates are valid for ~6 days and will auto-renew.${plain}" echo -e "${yellow}Default listener is port 80. If you choose another port, ensure external port 80 forwards to it.${plain}" # Check for acme.sh if ! command -v ~/.acme.sh/acme.sh &>/dev/null; then install_acme if [ $? -ne 0 ]; then echo -e "${red}Failed to install acme.sh${plain}" return 1 fi fi # Validate IP address if [[ -z "$ipv4" ]]; then echo -e "${red}IPv4 address is required${plain}" return 1 fi if ! is_ipv4 "$ipv4"; then echo -e "${red}Invalid IPv4 address: $ipv4${plain}" return 1 fi # Create certificate directory local certDir="/root/cert/ip" mkdir -p "$certDir" # Build domain arguments local domain_args="-d ${ipv4}" if [[ -n "$ipv6" ]] && is_ipv6 "$ipv6"; then domain_args="${domain_args} -d ${ipv6}" echo -e "${green}Including IPv6 address: ${ipv6}${plain}" fi # Set reload command for auto-renewal (add || true so it doesn't fail if service stopped) local reloadCmd="systemctl restart x-ui 2>/dev/null || rc-service x-ui restart 2>/dev/null || true" # Choose port for HTTP-01 listener (default 80, prompt override) local WebPort="" read -rp "Port to use for ACME HTTP-01 listener (default 80): " WebPort WebPort="${WebPort:-80}" if ! [[ "${WebPort}" =~ ^[0-9]+$ ]] || ((WebPort < 1 || WebPort > 65535)); then echo -e "${red}Invalid port provided. Falling back to 80.${plain}" WebPort=80 fi echo -e "${green}Using port ${WebPort} for standalone validation.${plain}" if [[ "${WebPort}" -ne 80 ]]; then echo -e "${yellow}Reminder: Let's Encrypt still connects on port 80; forward external port 80 to ${WebPort}.${plain}" fi # Ensure chosen port is available while true; do if is_port_in_use "${WebPort}"; then echo -e "${yellow}Port ${WebPort} is currently in use.${plain}" local alt_port="" read -rp "Enter another port for acme.sh standalone listener (leave empty to abort): " alt_port alt_port="${alt_port// /}" if [[ -z "${alt_port}" ]]; then echo -e "${red}Port ${WebPort} is busy; cannot proceed.${plain}" return 1 fi if ! [[ "${alt_port}" =~ ^[0-9]+$ ]] || ((alt_port < 1 || alt_port > 65535)); then echo -e "${red}Invalid port provided.${plain}" return 1 fi WebPort="${alt_port}" continue else echo -e "${green}Port ${WebPort} is free and ready for standalone validation.${plain}" break fi done # Issue certificate with shortlived profile echo -e "${green}Issuing IP certificate for ${ipv4}...${plain}" ~/.acme.sh/acme.sh --set-default-ca --server letsencrypt --force >/dev/null 2>&1 ~/.acme.sh/acme.sh --issue \ ${domain_args} \ --standalone \ --server letsencrypt \ --certificate-profile shortlived \ --days 6 \ --httpport ${WebPort} \ --force if [ $? -ne 0 ]; then echo -e "${red}Failed to issue IP certificate${plain}" echo -e "${yellow}Please ensure port ${WebPort} is reachable (or forwarded from external port 80)${plain}" # Cleanup acme.sh data for both IPv4 and IPv6 if specified rm -rf ~/.acme.sh/${ipv4} 2>/dev/null [[ -n "$ipv6" ]] && rm -rf ~/.acme.sh/${ipv6} 2>/dev/null rm -rf ${certDir} 2>/dev/null return 1 fi echo -e "${green}Certificate issued successfully, installing...${plain}" # Install certificate # Note: acme.sh may report "Reload error" and exit non-zero if reloadcmd fails, # but the cert files are still installed. We check for files instead of exit code. ~/.acme.sh/acme.sh --installcert -d ${ipv4} \ --key-file "${certDir}/privkey.pem" \ --fullchain-file "${certDir}/fullchain.pem" \ --reloadcmd "${reloadCmd}" 2>&1 || true # Verify certificate files exist (don't rely on exit code - reloadcmd failure causes non-zero) if [[ ! -f "${certDir}/fullchain.pem" || ! -f "${certDir}/privkey.pem" ]]; then echo -e "${red}Certificate files not found after installation${plain}" # Cleanup acme.sh data for both IPv4 and IPv6 if specified rm -rf ~/.acme.sh/${ipv4} 2>/dev/null [[ -n "$ipv6" ]] && rm -rf ~/.acme.sh/${ipv6} 2>/dev/null rm -rf ${certDir} 2>/dev/null return 1 fi echo -e "${green}Certificate files installed successfully${plain}" # Enable auto-upgrade for acme.sh (ensures cron job runs) ~/.acme.sh/acme.sh --upgrade --auto-upgrade >/dev/null 2>&1 chmod 600 ${certDir}/privkey.pem 2>/dev/null chmod 644 ${certDir}/fullchain.pem 2>/dev/null # Configure panel to use the certificate echo -e "${green}Setting certificate paths for the panel...${plain}" ${xui_folder}/x-ui cert -webCert "${certDir}/fullchain.pem" -webCertKey "${certDir}/privkey.pem" if [ $? -ne 0 ]; then echo -e "${yellow}Warning: Could not set certificate paths automatically.${plain}" echo -e "${yellow}You may need to set them manually in the panel settings.${plain}" echo -e "${yellow}Cert path: ${certDir}/fullchain.pem${plain}" echo -e "${yellow}Key path: ${certDir}/privkey.pem${plain}" else echo -e "${green}Certificate paths set successfully!${plain}" fi echo -e "${green}IP certificate installed and configured successfully!${plain}" echo -e "${green}Certificate valid for ~6 days, auto-renews via acme.sh cron job.${plain}" echo -e "${yellow}Panel will automatically restart after each renewal.${plain}" return 0 } # Comprehensive manual SSL certificate issuance via acme.sh ssl_cert_issue() { local existing_webBasePath=$(${xui_folder}/x-ui setting -show true | grep 'webBasePath:' | awk -F': ' '{print $2}' | tr -d '[:space:]' | sed 's#^/##') local existing_port=$(${xui_folder}/x-ui setting -show true | grep 'port:' | awk -F': ' '{print $2}' | tr -d '[:space:]') # check for acme.sh first if ! command -v ~/.acme.sh/acme.sh &>/dev/null; then echo "acme.sh could not be found. Installing now..." cd ~ || return 1 curl -s https://get.acme.sh | sh if [ $? -ne 0 ]; then echo -e "${red}Failed to install acme.sh${plain}" return 1 else echo -e "${green}acme.sh installed successfully${plain}" fi fi # get the domain here, and we need to verify it local domain="" while true; do read -rp "Please enter your domain name: " domain domain="${domain// /}" # Trim whitespace if [[ -z "$domain" ]]; then echo -e "${red}Domain name cannot be empty. Please try again.${plain}" continue fi if ! is_domain "$domain"; then echo -e "${red}Invalid domain format: ${domain}. Please enter a valid domain name.${plain}" continue fi break done echo -e "${green}Your domain is: ${domain}, checking it...${plain}" # check if there already exists a certificate local currentCert=$(~/.acme.sh/acme.sh --list | tail -1 | awk '{print $1}') if [ "${currentCert}" == "${domain}" ]; then local certInfo=$(~/.acme.sh/acme.sh --list) echo -e "${red}System already has certificates for this domain. Cannot issue again.${plain}" echo -e "${yellow}Current certificate details:${plain}" echo "$certInfo" return 1 else echo -e "${green}Your domain is ready for issuing certificates now...${plain}" fi # create a directory for the certificate certPath="/root/cert/${domain}" if [ ! -d "$certPath" ]; then mkdir -p "$certPath" else rm -rf "$certPath" mkdir -p "$certPath" fi # get the port number for the standalone server local WebPort=80 read -rp "Please choose which port to use (default is 80): " WebPort if [[ ${WebPort} -gt 65535 || ${WebPort} -lt 1 ]]; then echo -e "${yellow}Your input ${WebPort} is invalid, will use default port 80.${plain}" WebPort=80 fi echo -e "${green}Will use port: ${WebPort} to issue certificates. Please make sure this port is open.${plain}" # Stop panel temporarily echo -e "${yellow}Stopping panel temporarily...${plain}" systemctl stop x-ui 2>/dev/null || rc-service x-ui stop 2>/dev/null # issue the certificate ~/.acme.sh/acme.sh --set-default-ca --server letsencrypt --force ~/.acme.sh/acme.sh --issue -d ${domain} --listen-v6 --standalone --httpport ${WebPort} --force if [ $? -ne 0 ]; then echo -e "${red}Issuing certificate failed, please check logs.${plain}" rm -rf ~/.acme.sh/${domain} systemctl start x-ui 2>/dev/null || rc-service x-ui start 2>/dev/null return 1 else echo -e "${green}Issuing certificate succeeded, installing certificates...${plain}" fi # Setup reload command reloadCmd="systemctl restart x-ui || rc-service x-ui restart" echo -e "${green}Default --reloadcmd for ACME is: ${yellow}systemctl restart x-ui || rc-service x-ui restart${plain}" echo -e "${green}This command will run on every certificate issue and renew.${plain}" read -rp "Would you like to modify --reloadcmd for ACME? (y/n): " setReloadcmd if [[ "$setReloadcmd" == "y" || "$setReloadcmd" == "Y" ]]; then echo -e "\n${green}\t1.${plain} Preset: systemctl reload nginx ; systemctl restart x-ui" echo -e "${green}\t2.${plain} Input your own command" echo -e "${green}\t0.${plain} Keep default reloadcmd" read -rp "Choose an option: " choice case "$choice" in 1) echo -e "${green}Reloadcmd is: systemctl reload nginx ; systemctl restart x-ui${plain}" reloadCmd="systemctl reload nginx ; systemctl restart x-ui" ;; 2) echo -e "${yellow}It's recommended to put x-ui restart at the end${plain}" read -rp "Please enter your custom reloadcmd: " reloadCmd echo -e "${green}Reloadcmd is: ${reloadCmd}${plain}" ;; *) echo -e "${green}Keeping default reloadcmd${plain}" ;; esac fi # install the certificate ~/.acme.sh/acme.sh --installcert -d ${domain} \ --key-file /root/cert/${domain}/privkey.pem \ --fullchain-file /root/cert/${domain}/fullchain.pem --reloadcmd "${reloadCmd}" if [ $? -ne 0 ]; then echo -e "${red}Installing certificate failed, exiting.${plain}" rm -rf ~/.acme.sh/${domain} systemctl start x-ui 2>/dev/null || rc-service x-ui start 2>/dev/null return 1 else echo -e "${green}Installing certificate succeeded, enabling auto renew...${plain}" fi # enable auto-renew ~/.acme.sh/acme.sh --upgrade --auto-upgrade if [ $? -ne 0 ]; then echo -e "${yellow}Auto renew setup had issues, certificate details:${plain}" ls -lah /root/cert/${domain}/ chmod 600 $certPath/privkey.pem chmod 644 $certPath/fullchain.pem else echo -e "${green}Auto renew succeeded, certificate details:${plain}" ls -lah /root/cert/${domain}/ chmod 600 $certPath/privkey.pem chmod 644 $certPath/fullchain.pem fi # Restart panel systemctl start x-ui 2>/dev/null || rc-service x-ui start 2>/dev/null # Prompt user to set panel paths after successful certificate installation read -rp "Would you like to set this certificate for the panel? (y/n): " setPanel if [[ "$setPanel" == "y" || "$setPanel" == "Y" ]]; then local webCertFile="/root/cert/${domain}/fullchain.pem" local webKeyFile="/root/cert/${domain}/privkey.pem" if [[ -f "$webCertFile" && -f "$webKeyFile" ]]; then ${xui_folder}/x-ui cert -webCert "$webCertFile" -webCertKey "$webKeyFile" echo -e "${green}Certificate paths set for the panel${plain}" echo -e "${green}Certificate File: $webCertFile${plain}" echo -e "${green}Private Key File: $webKeyFile${plain}" echo "" echo -e "${green}Access URL: https://${domain}:${existing_port}/${existing_webBasePath}${plain}" echo -e "${yellow}Panel will restart to apply SSL certificate...${plain}" systemctl restart x-ui 2>/dev/null || rc-service x-ui restart 2>/dev/null else echo -e "${red}Error: Certificate or private key file not found for domain: $domain.${plain}" fi else echo -e "${yellow}Skipping panel path setting.${plain}" fi return 0 } # Unified interactive SSL setup (domain or IP) # Sets global `SSL_HOST` to the chosen domain/IP prompt_and_setup_ssl() { local panel_port="$1" local web_base_path="$2" # expected without leading slash local server_ip="$3" local ssl_choice="" echo -e "${yellow}Choose SSL certificate setup method:${plain}" echo -e "${green}1.${plain} Let's Encrypt for Domain (90-day validity, auto-renews)" echo -e "${green}2.${plain} Let's Encrypt for IP Address (6-day validity, auto-renews)" echo -e "${green}3.${plain} Custom SSL Certificate (Path to existing files)" echo -e "${blue}Note:${plain} Options 1 & 2 require port 80 open. Option 3 requires manual paths." read -rp "Choose an option (default 2 for IP): " ssl_choice ssl_choice="${ssl_choice// /}" # Trim whitespace # Default to 2 (IP cert) if input is empty or invalid (not 1 or 3) if [[ "$ssl_choice" != "1" && "$ssl_choice" != "3" ]]; then ssl_choice="2" fi case "$ssl_choice" in 1) # User chose Let's Encrypt domain option echo -e "${green}Using Let's Encrypt for domain certificate...${plain}" ssl_cert_issue # Extract the domain that was used from the certificate local cert_domain=$(~/.acme.sh/acme.sh --list 2>/dev/null | tail -1 | awk '{print $1}') if [[ -n "${cert_domain}" ]]; then SSL_HOST="${cert_domain}" echo -e "${green}✓ SSL certificate configured successfully with domain: ${cert_domain}${plain}" else echo -e "${yellow}SSL setup may have completed, but domain extraction failed${plain}" SSL_HOST="${server_ip}" fi ;; 2) # User chose Let's Encrypt IP certificate option echo -e "${green}Using Let's Encrypt for IP certificate (shortlived profile)...${plain}" # Ask for optional IPv6 local ipv6_addr="" read -rp "Do you have an IPv6 address to include? (leave empty to skip): " ipv6_addr ipv6_addr="${ipv6_addr// /}" # Trim whitespace # Stop panel if running (port 80 needed) if [[ $release == "alpine" ]]; then rc-service x-ui stop >/dev/null 2>&1 else systemctl stop x-ui >/dev/null 2>&1 fi setup_ip_certificate "${server_ip}" "${ipv6_addr}" if [ $? -eq 0 ]; then SSL_HOST="${server_ip}" echo -e "${green}✓ Let's Encrypt IP certificate configured successfully${plain}" else echo -e "${red}✗ IP certificate setup failed. Please check port 80 is open.${plain}" SSL_HOST="${server_ip}" fi # Restart panel after SSL is configured (restart applies new cert settings) if [[ $release == "alpine" ]]; then rc-service x-ui restart >/dev/null 2>&1 else systemctl restart x-ui >/dev/null 2>&1 fi ;; 3) # User chose Custom Paths (User Provided) option echo -e "${green}Using custom existing certificate...${plain}" local custom_cert="" local custom_key="" local custom_domain="" # 3.1 Request Domain to compose Panel URL later read -rp "Please enter domain name certificate issued for: " custom_domain custom_domain="${custom_domain// /}" # Убираем пробелы # 3.2 Loop for Certificate Path while true; do read -rp "Input certificate path (keywords: .crt / fullchain): " custom_cert # Strip quotes if present custom_cert=$(echo "$custom_cert" | tr -d '"' | tr -d "'") if [[ -f "$custom_cert" && -r "$custom_cert" && -s "$custom_cert" ]]; then break elif [[ ! -f "$custom_cert" ]]; then echo -e "${red}Error: File does not exist! Try again.${plain}" elif [[ ! -r "$custom_cert" ]]; then echo -e "${red}Error: File exists but is not readable (check permissions)!${plain}" else echo -e "${red}Error: File is empty!${plain}" fi done # 3.3 Loop for Private Key Path while true; do read -rp "Input private key path (keywords: .key / privatekey): " custom_key # Strip quotes if present custom_key=$(echo "$custom_key" | tr -d '"' | tr -d "'") if [[ -f "$custom_key" && -r "$custom_key" && -s "$custom_key" ]]; then break elif [[ ! -f "$custom_key" ]]; then echo -e "${red}Error: File does not exist! Try again.${plain}" elif [[ ! -r "$custom_key" ]]; then echo -e "${red}Error: File exists but is not readable (check permissions)!${plain}" else echo -e "${red}Error: File is empty!${plain}" fi done # 3.4 Apply Settings via x-ui binary ${xui_folder}/x-ui cert -webCert "$custom_cert" -webCertKey "$custom_key" >/dev/null 2>&1 # Set SSL_HOST for composing Panel URL if [[ -n "$custom_domain" ]]; then SSL_HOST="$custom_domain" else SSL_HOST="${server_ip}" fi echo -e "${green}✓ Custom certificate paths applied.${plain}" echo -e "${yellow}Note: You are responsible for renewing these files externally.${plain}" systemctl restart x-ui >/dev/null 2>&1 || rc-service x-ui restart >/dev/null 2>&1 ;; *) echo -e "${red}Invalid option. Skipping SSL setup.${plain}" SSL_HOST="${server_ip}" ;; esac } config_after_update() { echo -e "${yellow}x-ui settings:${plain}" ${xui_folder}/x-ui setting -show true ${xui_folder}/x-ui migrate # Properly detect empty cert by checking if cert: line exists and has content after it local existing_cert=$(${xui_folder}/x-ui setting -getCert true 2>/dev/null | grep 'cert:' | awk -F': ' '{print $2}' | tr -d '[:space:]') local existing_port=$(${xui_folder}/x-ui setting -show true | grep -Eo 'port: .+' | awk '{print $2}') local existing_webBasePath=$(${xui_folder}/x-ui setting -show true | grep -Eo 'webBasePath: .+' | awk '{print $2}' | sed 's#^/##') # Get server IP local URL_lists=( "https://api4.ipify.org" "https://ipv4.icanhazip.com" "https://v4.api.ipinfo.io/ip" "https://ipv4.myexternalip.com/raw" "https://4.ident.me" "https://check-host.net/ip" ) local server_ip="" for ip_address in "${URL_lists[@]}"; do local response=$(curl -s -w "\n%{http_code}" --max-time 3 "${ip_address}" 2>/dev/null) local http_code=$(echo "$response" | tail -n1) local ip_result=$(echo "$response" | head -n-1 | tr -d '[:space:]') if [[ "${http_code}" == "200" && -n "${ip_result}" ]]; then server_ip="${ip_result}" break fi done # Handle missing/short webBasePath if [[ ${#existing_webBasePath} -lt 4 ]]; then echo -e "${yellow}WebBasePath is missing or too short. Generating a new one...${plain}" local config_webBasePath=$(gen_random_string 18) ${xui_folder}/x-ui setting -webBasePath "${config_webBasePath}" existing_webBasePath="${config_webBasePath}" echo -e "${green}New WebBasePath: ${config_webBasePath}${plain}" fi # Check and prompt for SSL if missing if [[ -z "$existing_cert" ]]; then echo "" echo -e "${red}═══════════════════════════════════════════${plain}" echo -e "${red} ⚠ NO SSL CERTIFICATE DETECTED ⚠ ${plain}" echo -e "${red}═══════════════════════════════════════════${plain}" echo -e "${yellow}For security, SSL certificate is MANDATORY for all panels.${plain}" echo -e "${yellow}Let's Encrypt now supports both domains and IP addresses!${plain}" echo "" if [[ -z "${server_ip}" ]]; then echo -e "${red}Failed to detect server IP${plain}" echo -e "${yellow}Please configure SSL manually using: x-ui${plain}" return fi # Prompt and setup SSL (domain or IP) prompt_and_setup_ssl "${existing_port}" "${existing_webBasePath}" "${server_ip}" echo "" echo -e "${green}═══════════════════════════════════════════${plain}" echo -e "${green} Panel Access Information ${plain}" echo -e "${green}═══════════════════════════════════════════${plain}" echo -e "${green}Access URL: https://${SSL_HOST}:${existing_port}/${existing_webBasePath}${plain}" echo -e "${green}═══════════════════════════════════════════${plain}" echo -e "${yellow}⚠ SSL Certificate: Enabled and configured${plain}" else echo -e "${green}SSL certificate is already configured${plain}" # Show access URL with existing certificate local cert_domain=$(basename "$(dirname "$existing_cert")") echo "" echo -e "${green}═══════════════════════════════════════════${plain}" echo -e "${green} Panel Access Information ${plain}" echo -e "${green}═══════════════════════════════════════════${plain}" echo -e "${green}Access URL: https://${cert_domain}:${existing_port}/${existing_webBasePath}${plain}" echo -e "${green}═══════════════════════════════════════════${plain}" fi } update_x-ui() { cd ${xui_folder%/x-ui}/ if [ -f "${xui_folder}/x-ui" ]; then current_xui_version=$(${xui_folder}/x-ui -v) echo -e "${green}Current x-ui version: ${current_xui_version}${plain}" else _fail "ERROR: Current x-ui version: unknown" fi echo -e "${green}Downloading new x-ui version...${plain}" tag_version=$(${curl_bin} -Ls "https://api.github.com/repos/MHSanaei/3x-ui/releases/latest" 2>/dev/null | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/') if [[ ! -n "$tag_version" ]]; then echo -e "${yellow}Trying to fetch version with IPv4...${plain}" tag_version=$(${curl_bin} -4 -Ls "https://api.github.com/repos/MHSanaei/3x-ui/releases/latest" | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/') if [[ ! -n "$tag_version" ]]; then _fail "ERROR: Failed to fetch x-ui version, it may be due to GitHub API restrictions, please try it later" fi fi echo -e "Got x-ui latest version: ${tag_version}, beginning the installation..." ${curl_bin} -fLRo ${xui_folder}-linux-$(arch).tar.gz https://github.com/MHSanaei/3x-ui/releases/download/${tag_version}/x-ui-linux-$(arch).tar.gz 2>/dev/null if [[ $? -ne 0 ]]; then echo -e "${yellow}Trying to fetch version with IPv4...${plain}" ${curl_bin} -4fLRo ${xui_folder}-linux-$(arch).tar.gz https://github.com/MHSanaei/3x-ui/releases/download/${tag_version}/x-ui-linux-$(arch).tar.gz 2>/dev/null if [[ $? -ne 0 ]]; then _fail "ERROR: Failed to download x-ui, please be sure that your server can access GitHub" fi fi if [[ -e ${xui_folder}/ ]]; then echo -e "${green}Stopping x-ui...${plain}" if [[ $release == "alpine" ]]; then if [ -f "/etc/init.d/x-ui" ]; then rc-service x-ui stop >/dev/null 2>&1 rc-update del x-ui >/dev/null 2>&1 echo -e "${green}Removing old service unit version...${plain}" rm -f /etc/init.d/x-ui >/dev/null 2>&1 else rm x-ui-linux-$(arch).tar.gz -f >/dev/null 2>&1 _fail "ERROR: x-ui service unit not installed." fi else if [ -f "${xui_service}/x-ui.service" ]; then systemctl stop x-ui >/dev/null 2>&1 systemctl disable x-ui >/dev/null 2>&1 echo -e "${green}Removing old systemd unit version...${plain}" rm ${xui_service}/x-ui.service -f >/dev/null 2>&1 systemctl daemon-reload >/dev/null 2>&1 else rm x-ui-linux-$(arch).tar.gz -f >/dev/null 2>&1 _fail "ERROR: x-ui systemd unit not installed." fi fi echo -e "${green}Removing old x-ui version...${plain}" rm ${xui_folder} -f >/dev/null 2>&1 rm ${xui_folder}/x-ui.service -f >/dev/null 2>&1 rm ${xui_folder}/x-ui.service.debian -f >/dev/null 2>&1 rm ${xui_folder}/x-ui.service.arch -f >/dev/null 2>&1 rm ${xui_folder}/x-ui.service.rhel -f >/dev/null 2>&1 rm ${xui_folder}/x-ui -f >/dev/null 2>&1 rm ${xui_folder}/x-ui.sh -f >/dev/null 2>&1 echo -e "${green}Removing old xray version...${plain}" rm ${xui_folder}/bin/xray-linux-amd64 -f >/dev/null 2>&1 echo -e "${green}Removing old README and LICENSE file...${plain}" rm ${xui_folder}/bin/README.md -f >/dev/null 2>&1 rm ${xui_folder}/bin/LICENSE -f >/dev/null 2>&1 else rm x-ui-linux-$(arch).tar.gz -f >/dev/null 2>&1 _fail "ERROR: x-ui not installed." fi echo -e "${green}Installing new x-ui version...${plain}" tar zxvf x-ui-linux-$(arch).tar.gz >/dev/null 2>&1 rm x-ui-linux-$(arch).tar.gz -f >/dev/null 2>&1 cd x-ui >/dev/null 2>&1 chmod +x x-ui >/dev/null 2>&1 # Check the system's architecture and rename the file accordingly if [[ $(arch) == "armv5" || $(arch) == "armv6" || $(arch) == "armv7" ]]; then mv bin/xray-linux-$(arch) bin/xray-linux-arm >/dev/null 2>&1 chmod +x bin/xray-linux-arm >/dev/null 2>&1 fi chmod +x x-ui bin/xray-linux-$(arch) >/dev/null 2>&1 echo -e "${green}Downloading and installing x-ui.sh script...${plain}" ${curl_bin} -fLRo /usr/bin/x-ui https://raw.githubusercontent.com/MHSanaei/3x-ui/main/x-ui.sh >/dev/null 2>&1 if [[ $? -ne 0 ]]; then echo -e "${yellow}Trying to fetch x-ui with IPv4...${plain}" ${curl_bin} -4fLRo /usr/bin/x-ui https://raw.githubusercontent.com/MHSanaei/3x-ui/main/x-ui.sh >/dev/null 2>&1 if [[ $? -ne 0 ]]; then _fail "ERROR: Failed to download x-ui.sh script, please be sure that your server can access GitHub" fi fi chmod +x ${xui_folder}/x-ui.sh >/dev/null 2>&1 chmod +x /usr/bin/x-ui >/dev/null 2>&1 mkdir -p /var/log/x-ui >/dev/null 2>&1 echo -e "${green}Changing owner...${plain}" chown -R root:root ${xui_folder} >/dev/null 2>&1 if [ -f "${xui_folder}/bin/config.json" ]; then echo -e "${green}Changing on config file permissions...${plain}" chmod 640 ${xui_folder}/bin/config.json >/dev/null 2>&1 fi if [[ $release == "alpine" ]]; then echo -e "${green}Downloading and installing startup unit x-ui.rc...${plain}" ${curl_bin} -fLRo /etc/init.d/x-ui https://raw.githubusercontent.com/MHSanaei/3x-ui/main/x-ui.rc >/dev/null 2>&1 if [[ $? -ne 0 ]]; then ${curl_bin} -4fLRo /etc/init.d/x-ui https://raw.githubusercontent.com/MHSanaei/3x-ui/main/x-ui.rc >/dev/null 2>&1 if [[ $? -ne 0 ]]; then _fail "ERROR: Failed to download startup unit x-ui.rc, please be sure that your server can access GitHub" fi fi chmod +x /etc/init.d/x-ui >/dev/null 2>&1 chown root:root /etc/init.d/x-ui >/dev/null 2>&1 rc-update add x-ui >/dev/null 2>&1 rc-service x-ui start >/dev/null 2>&1 else if [ -f "x-ui.service" ]; then echo -e "${green}Installing systemd unit...${plain}" cp -f x-ui.service ${xui_service}/ >/dev/null 2>&1 if [[ $? -ne 0 ]]; then echo -e "${red}Failed to copy x-ui.service${plain}" exit 1 fi else service_installed=false case "${release}" in ubuntu | debian | armbian) if [ -f "x-ui.service.debian" ]; then echo -e "${green}Installing debian-like systemd unit...${plain}" cp -f x-ui.service.debian ${xui_service}/x-ui.service >/dev/null 2>&1 if [[ $? -eq 0 ]]; then service_installed=true fi fi ;; arch | manjaro | parch) if [ -f "x-ui.service.arch" ]; then echo -e "${green}Installing arch-like systemd unit...${plain}" cp -f x-ui.service.arch ${xui_service}/x-ui.service >/dev/null 2>&1 if [[ $? -eq 0 ]]; then service_installed=true fi fi ;; *) if [ -f "x-ui.service.rhel" ]; then echo -e "${green}Installing rhel-like systemd unit...${plain}" cp -f x-ui.service.rhel ${xui_service}/x-ui.service >/dev/null 2>&1 if [[ $? -eq 0 ]]; then service_installed=true fi fi ;; esac # If service file not found in tar.gz, download from GitHub if [ "$service_installed" = false ]; then echo -e "${yellow}Service files not found in tar.gz, downloading from GitHub...${plain}" case "${release}" in ubuntu | debian | armbian) ${curl_bin} -4fLRo ${xui_service}/x-ui.service https://raw.githubusercontent.com/MHSanaei/3x-ui/main/x-ui.service.debian >/dev/null 2>&1 ;; arch | manjaro | parch) ${curl_bin} -4fLRo ${xui_service}/x-ui.service https://raw.githubusercontent.com/MHSanaei/3x-ui/main/x-ui.service.arch >/dev/null 2>&1 ;; *) ${curl_bin} -4fLRo ${xui_service}/x-ui.service https://raw.githubusercontent.com/MHSanaei/3x-ui/main/x-ui.service.rhel >/dev/null 2>&1 ;; esac if [[ $? -ne 0 ]]; then echo -e "${red}Failed to install x-ui.service from GitHub${plain}" exit 1 fi fi fi chown root:root ${xui_service}/x-ui.service >/dev/null 2>&1 chmod 644 ${xui_service}/x-ui.service >/dev/null 2>&1 systemctl daemon-reload >/dev/null 2>&1 systemctl enable x-ui >/dev/null 2>&1 systemctl start x-ui >/dev/null 2>&1 fi config_after_update echo -e "${green}x-ui ${tag_version}${plain} updating finished, it is running now..." echo -e "" echo -e "┌───────────────────────────────────────────────────────┐ │ ${blue}x-ui control menu usages (subcommands):${plain} │ │ │ │ ${blue}x-ui${plain} - Admin Management Script │ │ ${blue}x-ui start${plain} - Start │ │ ${blue}x-ui stop${plain} - Stop │ │ ${blue}x-ui restart${plain} - Restart │ │ ${blue}x-ui status${plain} - Current Status │ │ ${blue}x-ui settings${plain} - Current Settings │ │ ${blue}x-ui enable${plain} - Enable Autostart on OS Startup │ │ ${blue}x-ui disable${plain} - Disable Autostart on OS Startup │ │ ${blue}x-ui log${plain} - Check logs │ │ ${blue}x-ui banlog${plain} - Check Fail2ban ban logs │ │ ${blue}x-ui update${plain} - Update │ │ ${blue}x-ui legacy${plain} - Legacy version │ │ ${blue}x-ui install${plain} - Install │ │ ${blue}x-ui uninstall${plain} - Uninstall │ └───────────────────────────────────────────────────────┘" } echo -e "${green}Running...${plain}" install_base update_x-ui $1 ================================================ FILE: util/common/err.go ================================================ // Package common provides common utility functions for error handling, formatting, and multi-error management. package common import ( "errors" "fmt" "github.com/mhsanaei/3x-ui/v2/logger" ) // NewErrorf creates a new error with formatted message. func NewErrorf(format string, a ...any) error { msg := fmt.Sprintf(format, a...) return errors.New(msg) } // NewError creates a new error from the given arguments. func NewError(a ...any) error { msg := fmt.Sprintln(a...) return errors.New(msg) } // Recover handles panic recovery and logs the panic error if a message is provided. func Recover(msg string) any { panicErr := recover() if panicErr != nil { if msg != "" { logger.Error(msg, "panic:", panicErr) } } return panicErr } ================================================ FILE: util/common/format.go ================================================ package common import ( "fmt" ) // FormatTraffic formats traffic bytes into human-readable units (B, KB, MB, GB, TB, PB). func FormatTraffic(trafficBytes int64) string { units := []string{"B", "KB", "MB", "GB", "TB", "PB"} unitIndex := 0 size := float64(trafficBytes) for size >= 1024 && unitIndex < len(units)-1 { size /= 1024 unitIndex++ } return fmt.Sprintf("%.2f%s", size, units[unitIndex]) } ================================================ FILE: util/common/multi_error.go ================================================ package common import ( "strings" ) // multiError represents a collection of errors. type multiError []error // Error returns a string representation of all errors joined with " | ". func (e multiError) Error() string { var r strings.Builder r.WriteString("multierr: ") for _, err := range e { r.WriteString(err.Error()) r.WriteString(" | ") } return r.String() } // Combine combines multiple errors into a single error, filtering out nil errors. func Combine(maybeError ...error) error { var errs multiError for _, err := range maybeError { if err != nil { errs = append(errs, err) } } if len(errs) == 0 { return nil } return errs } ================================================ FILE: util/crypto/crypto.go ================================================ // Package crypto provides cryptographic utilities for password hashing and verification. package crypto import ( "golang.org/x/crypto/bcrypt" ) // HashPasswordAsBcrypt generates a bcrypt hash of the given password. func HashPasswordAsBcrypt(password string) (string, error) { hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) return string(hash), err } // CheckPasswordHash verifies if the given password matches the bcrypt hash. func CheckPasswordHash(hash, password string) bool { return bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)) == nil } ================================================ FILE: util/json_util/json.go ================================================ // Package json_util provides JSON utilities including a custom RawMessage type. package json_util import ( "errors" ) // RawMessage is a custom JSON raw message type that marshals empty slices as "null". type RawMessage []byte // MarshalJSON customizes the JSON marshaling behavior for RawMessage. // Empty RawMessage values are marshaled as "null" instead of "[]". func (m RawMessage) MarshalJSON() ([]byte, error) { if len(m) == 0 { return []byte("null"), nil } return m, nil } // UnmarshalJSON sets *m to a copy of the JSON data. func (m *RawMessage) UnmarshalJSON(data []byte) error { if m == nil { return errors.New("json.RawMessage: UnmarshalJSON on nil pointer") } *m = append((*m)[0:0], data...) return nil } ================================================ FILE: util/ldap/ldap.go ================================================ package ldaputil import ( "crypto/tls" "fmt" "github.com/go-ldap/ldap/v3" ) type Config struct { Host string Port int UseTLS bool BindDN string Password string BaseDN string UserFilter string UserAttr string FlagField string TruthyVals []string Invert bool } // FetchVlessFlags returns map[email]enabled func FetchVlessFlags(cfg Config) (map[string]bool, error) { addr := fmt.Sprintf("%s:%d", cfg.Host, cfg.Port) scheme := "ldap" if cfg.UseTLS { scheme = "ldaps" } ldapURL := fmt.Sprintf("%s://%s", scheme, addr) var opts []ldap.DialOpt if cfg.UseTLS { opts = append(opts, ldap.DialWithTLSConfig(&tls.Config{ InsecureSkipVerify: false, })) } conn, err := ldap.DialURL(ldapURL, opts...) if err != nil { return nil, err } defer conn.Close() if cfg.BindDN != "" { if err := conn.Bind(cfg.BindDN, cfg.Password); err != nil { return nil, err } } if cfg.UserFilter == "" { cfg.UserFilter = "(objectClass=person)" } if cfg.UserAttr == "" { cfg.UserAttr = "mail" } // if field not set we fallback to legacy vless_enabled if cfg.FlagField == "" { cfg.FlagField = "vless_enabled" } req := ldap.NewSearchRequest( cfg.BaseDN, ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false, cfg.UserFilter, []string{cfg.UserAttr, cfg.FlagField}, nil, ) res, err := conn.Search(req) if err != nil { return nil, err } result := make(map[string]bool, len(res.Entries)) for _, e := range res.Entries { user := e.GetAttributeValue(cfg.UserAttr) if user == "" { continue } val := e.GetAttributeValue(cfg.FlagField) enabled := false for _, t := range cfg.TruthyVals { if val == t { enabled = true break } } if cfg.Invert { enabled = !enabled } result[user] = enabled } return result, nil } // AuthenticateUser searches user by cfg.UserAttr and attempts to bind with provided password. func AuthenticateUser(cfg Config, username, password string) (bool, error) { addr := fmt.Sprintf("%s:%d", cfg.Host, cfg.Port) scheme := "ldap" if cfg.UseTLS { scheme = "ldaps" } ldapURL := fmt.Sprintf("%s://%s", scheme, addr) var opts []ldap.DialOpt if cfg.UseTLS { opts = append(opts, ldap.DialWithTLSConfig(&tls.Config{ InsecureSkipVerify: false, })) } conn, err := ldap.DialURL(ldapURL, opts...) if err != nil { return false, err } defer conn.Close() // Optional initial bind for search if cfg.BindDN != "" { if err := conn.Bind(cfg.BindDN, cfg.Password); err != nil { return false, err } } if cfg.UserFilter == "" { cfg.UserFilter = "(objectClass=person)" } if cfg.UserAttr == "" { cfg.UserAttr = "uid" } // Build filter to find specific user filter := fmt.Sprintf("(&%s(%s=%s))", cfg.UserFilter, cfg.UserAttr, ldap.EscapeFilter(username)) req := ldap.NewSearchRequest( cfg.BaseDN, ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 1, 0, false, filter, []string{"dn"}, nil, ) res, err := conn.Search(req) if err != nil { return false, err } if len(res.Entries) == 0 { return false, nil } userDN := res.Entries[0].DN // Try to bind as the user if err := conn.Bind(userDN, password); err != nil { return false, nil } return true, nil } ================================================ FILE: util/random/random.go ================================================ // Package random provides utilities for generating random strings and numbers. package random import ( "crypto/rand" "math/big" ) var ( numSeq [10]rune lowerSeq [26]rune upperSeq [26]rune numLowerSeq [36]rune numUpperSeq [36]rune allSeq [62]rune ) // init initializes the character sequences used for random string generation. // It sets up arrays for numbers, lowercase letters, uppercase letters, and combinations. func init() { for i := range 10 { numSeq[i] = rune('0' + i) } for i := range 26 { lowerSeq[i] = rune('a' + i) upperSeq[i] = rune('A' + i) } copy(numLowerSeq[:], numSeq[:]) copy(numLowerSeq[len(numSeq):], lowerSeq[:]) copy(numUpperSeq[:], numSeq[:]) copy(numUpperSeq[len(numSeq):], upperSeq[:]) copy(allSeq[:], numSeq[:]) copy(allSeq[len(numSeq):], lowerSeq[:]) copy(allSeq[len(numSeq)+len(lowerSeq):], upperSeq[:]) } // Seq generates a random string of length n containing alphanumeric characters (numbers, lowercase and uppercase letters). func Seq(n int) string { runes := make([]rune, n) for i := range n { idx, err := rand.Int(rand.Reader, big.NewInt(int64(len(allSeq)))) if err != nil { panic("crypto/rand failed: " + err.Error()) } runes[i] = allSeq[idx.Int64()] } return string(runes) } // Num generates a random integer between 0 and n-1. func Num(n int) int { bn := big.NewInt(int64(n)) r, err := rand.Int(rand.Reader, bn) if err != nil { panic("crypto/rand failed: " + err.Error()) } return int(r.Int64()) } ================================================ FILE: util/reflect_util/reflect.go ================================================ // Package reflect_util provides reflection utilities for working with struct fields and values. package reflect_util import "reflect" // GetFields returns all struct fields of the given reflect.Type. func GetFields(t reflect.Type) []reflect.StructField { num := t.NumField() fields := make([]reflect.StructField, 0, num) for i := range num { fields = append(fields, t.Field(i)) } return fields } // GetFieldValues returns all field values of the given reflect.Value. func GetFieldValues(v reflect.Value) []reflect.Value { num := v.NumField() fields := make([]reflect.Value, 0, num) for i := range num { fields = append(fields, v.Field(i)) } return fields } ================================================ FILE: util/sys/psutil.go ================================================ // Package sys provides system utilities for monitoring network connections and CPU usage. // Platform-specific implementations are provided for Windows, Linux, and macOS. package sys import ( _ "unsafe" ) //go:linkname HostProc github.com/shirou/gopsutil/v4/internal/common.HostProc func HostProc(combineWith ...string) string ================================================ FILE: util/sys/sys_darwin.go ================================================ //go:build darwin // +build darwin package sys import ( "encoding/binary" "fmt" "sync" "syscall" "github.com/shirou/gopsutil/v4/net" "golang.org/x/sys/unix" ) var SIGUSR1 = syscall.SIGUSR1 func GetTCPCount() (int, error) { stats, err := net.Connections("tcp") if err != nil { return 0, err } return len(stats), nil } func GetUDPCount() (int, error) { stats, err := net.Connections("udp") if err != nil { return 0, err } return len(stats), nil } // --- CPU Utilization (macOS native) --- // sysctl kern.cp_time returns an array of 5 longs: user, nice, sys, idle, intr. // We compute utilization deltas without cgo. var ( cpuMu sync.Mutex lastTotals [5]uint64 hasLastCPUT bool ) func CPUPercentRaw() (float64, error) { raw, err := unix.SysctlRaw("kern.cp_time") if err != nil { return 0, err } // Expect either 5*8 bytes (uint64) or 5*4 bytes (uint32) var out [5]uint64 switch len(raw) { case 5 * 8: for i := range 5 { out[i] = binary.LittleEndian.Uint64(raw[i*8 : (i+1)*8]) } case 5 * 4: for i := range 5 { out[i] = uint64(binary.LittleEndian.Uint32(raw[i*4 : (i+1)*4])) } default: return 0, fmt.Errorf("unexpected kern.cp_time size: %d", len(raw)) } // user, nice, sys, idle, intr user := out[0] nice := out[1] sysv := out[2] idle := out[3] intr := out[4] cpuMu.Lock() defer cpuMu.Unlock() if !hasLastCPUT { lastTotals = out hasLastCPUT = true return 0, nil } dUser := user - lastTotals[0] dNice := nice - lastTotals[1] dSys := sysv - lastTotals[2] dIdle := idle - lastTotals[3] dIntr := intr - lastTotals[4] lastTotals = out totald := dUser + dNice + dSys + dIdle + dIntr if totald == 0 { return 0, nil } busy := totald - dIdle pct := float64(busy) / float64(totald) * 100.0 if pct > 100 { pct = 100 } return pct, nil } ================================================ FILE: util/sys/sys_linux.go ================================================ //go:build linux // +build linux package sys import ( "bufio" "bytes" "fmt" "io" "os" "strconv" "strings" "sync" "syscall" ) var SIGUSR1 = syscall.SIGUSR1 func getLinesNum(filename string) (int, error) { file, err := os.Open(filename) if err != nil { return 0, err } defer file.Close() sum := 0 buf := make([]byte, 8192) for { n, err := file.Read(buf) var buffPosition int for { i := bytes.IndexByte(buf[buffPosition:n], '\n') if i < 0 { break } buffPosition += i + 1 sum++ } if err == io.EOF { break } else if err != nil { return 0, err } } return sum, nil } // GetTCPCount returns the number of active TCP connections by reading // /proc/net/tcp and /proc/net/tcp6 when available. func GetTCPCount() (int, error) { root := HostProc() tcp4, err := safeGetLinesNum(fmt.Sprintf("%v/net/tcp", root)) if err != nil { return 0, err } tcp6, err := safeGetLinesNum(fmt.Sprintf("%v/net/tcp6", root)) if err != nil { return 0, err } return tcp4 + tcp6, nil } func GetUDPCount() (int, error) { root := HostProc() udp4, err := safeGetLinesNum(fmt.Sprintf("%v/net/udp", root)) if err != nil { return 0, err } udp6, err := safeGetLinesNum(fmt.Sprintf("%v/net/udp6", root)) if err != nil { return 0, err } return udp4 + udp6, nil } // safeGetLinesNum returns 0 if the file does not exist, otherwise forwards // to getLinesNum to count the number of lines. func safeGetLinesNum(path string) (int, error) { if _, err := os.Stat(path); os.IsNotExist(err) { return 0, nil } else if err != nil { return 0, err } return getLinesNum(path) } // --- CPU Utilization (Linux native) --- var ( cpuMu sync.Mutex lastTotal uint64 lastIdleAll uint64 hasLast bool ) // CPUPercentRaw returns instantaneous total CPU utilization by reading /proc/stat. // First call initializes and returns 0; subsequent calls return busy/total * 100. func CPUPercentRaw() (float64, error) { f, err := os.Open("/proc/stat") if err != nil { return 0, err } defer f.Close() rd := bufio.NewReader(f) line, err := rd.ReadString('\n') if err != nil && err != io.EOF { return 0, err } // Expect line like: cpu user nice system idle iowait irq softirq steal guest guest_nice fields := strings.Fields(line) if len(fields) < 5 || fields[0] != "cpu" { return 0, fmt.Errorf("unexpected /proc/stat format") } var nums []uint64 for i := 1; i < len(fields); i++ { v, err := strconv.ParseUint(fields[i], 10, 64) if err != nil { break } nums = append(nums, v) } if len(nums) < 4 { // need at least user,nice,system,idle return 0, fmt.Errorf("insufficient cpu fields") } // Conform with standard Linux CPU accounting var user, nice, system, idle, iowait, irq, softirq, steal uint64 user = nums[0] if len(nums) > 1 { nice = nums[1] } if len(nums) > 2 { system = nums[2] } if len(nums) > 3 { idle = nums[3] } if len(nums) > 4 { iowait = nums[4] } if len(nums) > 5 { irq = nums[5] } if len(nums) > 6 { softirq = nums[6] } if len(nums) > 7 { steal = nums[7] } idleAll := idle + iowait nonIdle := user + nice + system + irq + softirq + steal total := idleAll + nonIdle cpuMu.Lock() defer cpuMu.Unlock() if !hasLast { lastTotal = total lastIdleAll = idleAll hasLast = true return 0, nil } totald := total - lastTotal idled := idleAll - lastIdleAll lastTotal = total lastIdleAll = idleAll if totald == 0 { return 0, nil } busy := totald - idled pct := float64(busy) / float64(totald) * 100.0 if pct > 100 { pct = 100 } return pct, nil } ================================================ FILE: util/sys/sys_windows.go ================================================ //go:build windows // +build windows package sys import ( "errors" "sync" "syscall" "unsafe" "github.com/shirou/gopsutil/v4/net" ) var SIGUSR1 = syscall.Signal(0) // GetConnectionCount returns the number of active connections for the specified protocol ("tcp" or "udp"). func GetConnectionCount(proto string) (int, error) { if proto != "tcp" && proto != "udp" { return 0, errors.New("invalid protocol") } stats, err := net.Connections(proto) if err != nil { return 0, err } return len(stats), nil } // GetTCPCount returns the number of active TCP connections. func GetTCPCount() (int, error) { return GetConnectionCount("tcp") } // GetUDPCount returns the number of active UDP connections. func GetUDPCount() (int, error) { return GetConnectionCount("udp") } // --- CPU Utilization (Windows native) --- var ( modKernel32 = syscall.NewLazyDLL("kernel32.dll") procGetSystemTimes = modKernel32.NewProc("GetSystemTimes") cpuMu sync.Mutex lastIdle uint64 lastKernel uint64 lastUser uint64 hasLast bool ) type filetime struct { LowDateTime uint32 HighDateTime uint32 } // ftToUint64 converts a Windows FILETIME-like struct to a uint64 for // arithmetic and delta calculations used by CPUPercentRaw. func ftToUint64(ft filetime) uint64 { return (uint64(ft.HighDateTime) << 32) | uint64(ft.LowDateTime) } // CPUPercentRaw returns the instantaneous total CPU utilization percentage using // Windows GetSystemTimes across all logical processors. The first call returns 0 // as it initializes the baseline. Subsequent calls compute deltas. func CPUPercentRaw() (float64, error) { var idleFT, kernelFT, userFT filetime r1, _, e1 := procGetSystemTimes.Call( uintptr(unsafe.Pointer(&idleFT)), uintptr(unsafe.Pointer(&kernelFT)), uintptr(unsafe.Pointer(&userFT)), ) if r1 == 0 { // failure if e1 != nil { return 0, e1 } return 0, syscall.GetLastError() } idle := ftToUint64(idleFT) kernel := ftToUint64(kernelFT) user := ftToUint64(userFT) cpuMu.Lock() defer cpuMu.Unlock() if !hasLast { lastIdle = idle lastKernel = kernel lastUser = user hasLast = true return 0, nil } idleDelta := idle - lastIdle kernelDelta := kernel - lastKernel userDelta := user - lastUser // Update for next call lastIdle = idle lastKernel = kernel lastUser = user total := kernelDelta + userDelta if total == 0 { return 0, nil } // On Windows, kernel time includes idle time; busy = total - idle busy := total - idleDelta pct := float64(busy) / float64(total) * 100.0 // lower bound not needed; ratios of uint64 are non-negative if pct > 100 { pct = 100 } return pct, nil } ================================================ FILE: web/assets/codemirror/fold/brace-fold.js ================================================ // CodeMirror, copyright (c) by Marijn Haverbeke and others // Distributed under an MIT license: https://codemirror.net/5/LICENSE (function(mod) { if (typeof exports == "object" && typeof module == "object") // CommonJS mod(require("../../lib/codemirror")); else if (typeof define == "function" && define.amd) // AMD define(["../../lib/codemirror"], mod); else // Plain browser env mod(CodeMirror); })(function(CodeMirror) { "use strict"; function bracketFolding(pairs) { return function(cm, start) { var line = start.line, lineText = cm.getLine(line); function findOpening(pair) { var tokenType; for (var at = start.ch, pass = 0;;) { var found = at <= 0 ? -1 : lineText.lastIndexOf(pair[0], at - 1); if (found == -1) { if (pass == 1) break; pass = 1; at = lineText.length; continue; } if (pass == 1 && found < start.ch) break; tokenType = cm.getTokenTypeAt(CodeMirror.Pos(line, found + 1)); if (!/^(comment|string)/.test(tokenType)) return {ch: found + 1, tokenType: tokenType, pair: pair}; at = found - 1; } } function findRange(found) { var count = 1, lastLine = cm.lastLine(), end, startCh = found.ch, endCh outer: for (var i = line; i <= lastLine; ++i) { var text = cm.getLine(i), pos = i == line ? startCh : 0; for (;;) { var nextOpen = text.indexOf(found.pair[0], pos), nextClose = text.indexOf(found.pair[1], pos); if (nextOpen < 0) nextOpen = text.length; if (nextClose < 0) nextClose = text.length; pos = Math.min(nextOpen, nextClose); if (pos == text.length) break; if (cm.getTokenTypeAt(CodeMirror.Pos(i, pos + 1)) == found.tokenType) { if (pos == nextOpen) ++count; else if (!--count) { end = i; endCh = pos; break outer; } } ++pos; } } if (end == null || line == end) return null return {from: CodeMirror.Pos(line, startCh), to: CodeMirror.Pos(end, endCh)}; } var found = [] for (var i = 0; i < pairs.length; i++) { var open = findOpening(pairs[i]) if (open) found.push(open) } found.sort(function(a, b) { return a.ch - b.ch }) for (var i = 0; i < found.length; i++) { var range = findRange(found[i]) if (range) return range } return null } } CodeMirror.registerHelper("fold", "brace", bracketFolding([["{", "}"], ["[", "]"]])); CodeMirror.registerHelper("fold", "brace-paren", bracketFolding([["{", "}"], ["[", "]"], ["(", ")"]])); CodeMirror.registerHelper("fold", "import", function(cm, start) { function hasImport(line) { if (line < cm.firstLine() || line > cm.lastLine()) return null; var start = cm.getTokenAt(CodeMirror.Pos(line, 1)); if (!/\S/.test(start.string)) start = cm.getTokenAt(CodeMirror.Pos(line, start.end + 1)); if (start.type != "keyword" || start.string != "import") return null; // Now find closing semicolon, return its position for (var i = line, e = Math.min(cm.lastLine(), line + 10); i <= e; ++i) { var text = cm.getLine(i), semi = text.indexOf(";"); if (semi != -1) return {startCh: start.end, end: CodeMirror.Pos(i, semi)}; } } var startLine = start.line, has = hasImport(startLine), prev; if (!has || hasImport(startLine - 1) || ((prev = hasImport(startLine - 2)) && prev.end.line == startLine - 1)) return null; for (var end = has.end;;) { var next = hasImport(end.line + 1); if (next == null) break; end = next.end; } return {from: cm.clipPos(CodeMirror.Pos(startLine, has.startCh + 1)), to: end}; }); CodeMirror.registerHelper("fold", "include", function(cm, start) { function hasInclude(line) { if (line < cm.firstLine() || line > cm.lastLine()) return null; var start = cm.getTokenAt(CodeMirror.Pos(line, 1)); if (!/\S/.test(start.string)) start = cm.getTokenAt(CodeMirror.Pos(line, start.end + 1)); if (start.type == "meta" && start.string.slice(0, 8) == "#include") return start.start + 8; } var startLine = start.line, has = hasInclude(startLine); if (has == null || hasInclude(startLine - 1) != null) return null; for (var end = startLine;;) { var next = hasInclude(end + 1); if (next == null) break; ++end; } return {from: CodeMirror.Pos(startLine, has + 1), to: cm.clipPos(CodeMirror.Pos(end))}; }); }); ================================================ FILE: web/assets/codemirror/fold/foldcode.js ================================================ // CodeMirror, copyright (c) by Marijn Haverbeke and others // Distributed under an MIT license: https://codemirror.net/5/LICENSE (function(mod) { if (typeof exports == "object" && typeof module == "object") // CommonJS mod(require("../../lib/codemirror")); else if (typeof define == "function" && define.amd) // AMD define(["../../lib/codemirror"], mod); else // Plain browser env mod(CodeMirror); })(function(CodeMirror) { "use strict"; function doFold(cm, pos, options, force) { if (options && options.call) { var finder = options; options = null; } else { var finder = getOption(cm, options, "rangeFinder"); } if (typeof pos == "number") pos = CodeMirror.Pos(pos, 0); var minSize = getOption(cm, options, "minFoldSize"); function getRange(allowFolded) { var range = finder(cm, pos); if (!range || range.to.line - range.from.line < minSize) return null; if (force === "fold") return range; var marks = cm.findMarksAt(range.from); for (var i = 0; i < marks.length; ++i) { if (marks[i].__isFold) { if (!allowFolded) return null; range.cleared = true; marks[i].clear(); } } return range; } var range = getRange(true); if (getOption(cm, options, "scanUp")) while (!range && pos.line > cm.firstLine()) { pos = CodeMirror.Pos(pos.line - 1, 0); range = getRange(false); } if (!range || range.cleared || force === "unfold") return; var myWidget = makeWidget(cm, options, range); CodeMirror.on(myWidget, "mousedown", function(e) { myRange.clear(); CodeMirror.e_preventDefault(e); }); var myRange = cm.markText(range.from, range.to, { replacedWith: myWidget, clearOnEnter: getOption(cm, options, "clearOnEnter"), __isFold: true }); myRange.on("clear", function(from, to) { CodeMirror.signal(cm, "unfold", cm, from, to); }); CodeMirror.signal(cm, "fold", cm, range.from, range.to); } function makeWidget(cm, options, range) { var widget = getOption(cm, options, "widget"); if (typeof widget == "function") { widget = widget(range.from, range.to); } if (typeof widget == "string") { var text = document.createTextNode(widget); widget = document.createElement("span"); widget.appendChild(text); widget.className = "CodeMirror-foldmarker"; } else if (widget) { widget = widget.cloneNode(true) } return widget; } // Clumsy backwards-compatible interface CodeMirror.newFoldFunction = function(rangeFinder, widget) { return function(cm, pos) { doFold(cm, pos, {rangeFinder: rangeFinder, widget: widget}); }; }; // New-style interface CodeMirror.defineExtension("foldCode", function(pos, options, force) { doFold(this, pos, options, force); }); CodeMirror.defineExtension("isFolded", function(pos) { var marks = this.findMarksAt(pos); for (var i = 0; i < marks.length; ++i) if (marks[i].__isFold) return true; }); CodeMirror.commands.toggleFold = function(cm) { cm.foldCode(cm.getCursor()); }; CodeMirror.commands.fold = function(cm) { cm.foldCode(cm.getCursor(), null, "fold"); }; CodeMirror.commands.unfold = function(cm) { cm.foldCode(cm.getCursor(), { scanUp: false }, "unfold"); }; CodeMirror.commands.foldAll = function(cm) { cm.operation(function() { for (var i = cm.firstLine(), e = cm.lastLine(); i <= e; i++) cm.foldCode(CodeMirror.Pos(i, 0), { scanUp: false }, "fold"); }); }; CodeMirror.commands.unfoldAll = function(cm) { cm.operation(function() { for (var i = cm.firstLine(), e = cm.lastLine(); i <= e; i++) cm.foldCode(CodeMirror.Pos(i, 0), { scanUp: false }, "unfold"); }); }; CodeMirror.registerHelper("fold", "combine", function() { var funcs = Array.prototype.slice.call(arguments, 0); return function(cm, start) { for (var i = 0; i < funcs.length; ++i) { var found = funcs[i](cm, start); if (found) return found; } }; }); CodeMirror.registerHelper("fold", "auto", function(cm, start) { var helpers = cm.getHelpers(start, "fold"); for (var i = 0; i < helpers.length; i++) { var cur = helpers[i](cm, start); if (cur) return cur; } }); var defaultOptions = { rangeFinder: CodeMirror.fold.auto, widget: "\u2194", minFoldSize: 0, scanUp: false, clearOnEnter: true }; CodeMirror.defineOption("foldOptions", null); function getOption(cm, options, name) { if (options && options[name] !== undefined) return options[name]; var editorOptions = cm.options.foldOptions; if (editorOptions && editorOptions[name] !== undefined) return editorOptions[name]; return defaultOptions[name]; } CodeMirror.defineExtension("foldOption", function(options, name) { return getOption(this, options, name); }); }); ================================================ FILE: web/assets/codemirror/fold/foldgutter.css ================================================ .CodeMirror-foldmarker { color: blue; text-shadow: #b9f 1px 1px 2px, #b9f -1px -1px 2px, #b9f 1px -1px 2px, #b9f -1px 1px 2px; font-family: arial; line-height: .3; cursor: pointer; } .CodeMirror-foldgutter { width: .7em; } .CodeMirror-foldgutter-open, .CodeMirror-foldgutter-folded { cursor: pointer; } .CodeMirror-foldgutter-open:after { content: "\25BE"; } .CodeMirror-foldgutter-folded:after { content: "\25B8"; } ================================================ FILE: web/assets/codemirror/fold/foldgutter.js ================================================ // CodeMirror, copyright (c) by Marijn Haverbeke and others // Distributed under an MIT license: https://codemirror.net/5/LICENSE (function(mod) { if (typeof exports == "object" && typeof module == "object") // CommonJS mod(require("../../lib/codemirror"), require("./foldcode")); else if (typeof define == "function" && define.amd) // AMD define(["../../lib/codemirror", "./foldcode"], mod); else // Plain browser env mod(CodeMirror); })(function(CodeMirror) { "use strict"; CodeMirror.defineOption("foldGutter", false, function(cm, val, old) { if (old && old != CodeMirror.Init) { cm.clearGutter(cm.state.foldGutter.options.gutter); cm.state.foldGutter = null; cm.off("gutterClick", onGutterClick); cm.off("changes", onChange); cm.off("viewportChange", onViewportChange); cm.off("fold", onFold); cm.off("unfold", onFold); cm.off("swapDoc", onChange); cm.off("optionChange", optionChange); } if (val) { cm.state.foldGutter = new State(parseOptions(val)); updateInViewport(cm); cm.on("gutterClick", onGutterClick); cm.on("changes", onChange); cm.on("viewportChange", onViewportChange); cm.on("fold", onFold); cm.on("unfold", onFold); cm.on("swapDoc", onChange); cm.on("optionChange", optionChange); } }); var Pos = CodeMirror.Pos; function State(options) { this.options = options; this.from = this.to = 0; } function parseOptions(opts) { if (opts === true) opts = {}; if (opts.gutter == null) opts.gutter = "CodeMirror-foldgutter"; if (opts.indicatorOpen == null) opts.indicatorOpen = "CodeMirror-foldgutter-open"; if (opts.indicatorFolded == null) opts.indicatorFolded = "CodeMirror-foldgutter-folded"; return opts; } function isFolded(cm, line) { var marks = cm.findMarks(Pos(line, 0), Pos(line + 1, 0)); for (var i = 0; i < marks.length; ++i) { if (marks[i].__isFold) { var fromPos = marks[i].find(-1); if (fromPos && fromPos.line === line) return marks[i]; } } } function marker(spec) { if (typeof spec == "string") { var elt = document.createElement("div"); elt.className = spec + " CodeMirror-guttermarker-subtle"; return elt; } else { return spec.cloneNode(true); } } function updateFoldInfo(cm, from, to) { var opts = cm.state.foldGutter.options, cur = from - 1; var minSize = cm.foldOption(opts, "minFoldSize"); var func = cm.foldOption(opts, "rangeFinder"); // we can reuse the built-in indicator element if its className matches the new state var clsFolded = typeof opts.indicatorFolded == "string" && classTest(opts.indicatorFolded); var clsOpen = typeof opts.indicatorOpen == "string" && classTest(opts.indicatorOpen); cm.eachLine(from, to, function(line) { ++cur; var mark = null; var old = line.gutterMarkers; if (old) old = old[opts.gutter]; if (isFolded(cm, cur)) { if (clsFolded && old && clsFolded.test(old.className)) return; mark = marker(opts.indicatorFolded); } else { var pos = Pos(cur, 0); var range = func && func(cm, pos); if (range && range.to.line - range.from.line >= minSize) { if (clsOpen && old && clsOpen.test(old.className)) return; mark = marker(opts.indicatorOpen); } } if (!mark && !old) return; cm.setGutterMarker(line, opts.gutter, mark); }); } // copied from CodeMirror/src/util/dom.js function classTest(cls) { return new RegExp("(^|\\s)" + cls + "(?:$|\\s)\\s*") } function updateInViewport(cm) { var vp = cm.getViewport(), state = cm.state.foldGutter; if (!state) return; cm.operation(function() { updateFoldInfo(cm, vp.from, vp.to); }); state.from = vp.from; state.to = vp.to; } function onGutterClick(cm, line, gutter) { var state = cm.state.foldGutter; if (!state) return; var opts = state.options; if (gutter != opts.gutter) return; var folded = isFolded(cm, line); if (folded) folded.clear(); else cm.foldCode(Pos(line, 0), opts); } function optionChange(cm, option) { if (option == "mode") onChange(cm) } function onChange(cm) { var state = cm.state.foldGutter; if (!state) return; var opts = state.options; state.from = state.to = 0; clearTimeout(state.changeUpdate); state.changeUpdate = setTimeout(function() { updateInViewport(cm); }, opts.foldOnChangeTimeSpan || 600); } function onViewportChange(cm) { var state = cm.state.foldGutter; if (!state) return; var opts = state.options; clearTimeout(state.changeUpdate); state.changeUpdate = setTimeout(function() { var vp = cm.getViewport(); if (state.from == state.to || vp.from - state.to > 20 || state.from - vp.to > 20) { updateInViewport(cm); } else { cm.operation(function() { if (vp.from < state.from) { updateFoldInfo(cm, vp.from, state.from); state.from = vp.from; } if (vp.to > state.to) { updateFoldInfo(cm, state.to, vp.to); state.to = vp.to; } }); } }, opts.updateViewportTimeSpan || 400); } function onFold(cm, from) { var state = cm.state.foldGutter; if (!state) return; var line = from.line; if (line >= state.from && line < state.to) updateFoldInfo(cm, line, line + 1); } }); ================================================ FILE: web/assets/codemirror/hint/javascript-hint.js ================================================ // CodeMirror, copyright (c) by Marijn Haverbeke and others // Distributed under an MIT license: https://codemirror.net/5/LICENSE (function(mod) { if (typeof exports == "object" && typeof module == "object") // CommonJS mod(require("../../lib/codemirror")); else if (typeof define == "function" && define.amd) // AMD define(["../../lib/codemirror"], mod); else // Plain browser env mod(CodeMirror); })(function(CodeMirror) { var Pos = CodeMirror.Pos; function forEach(arr, f) { for (var i = 0, e = arr.length; i < e; ++i) f(arr[i]); } function arrayContains(arr, item) { if (!Array.prototype.indexOf) { var i = arr.length; while (i--) { if (arr[i] === item) { return true; } } return false; } return arr.indexOf(item) != -1; } function scriptHint(editor, keywords, getToken, options) { // Find the token at the cursor var cur = editor.getCursor(), token = getToken(editor, cur); if (/\b(?:string|comment)\b/.test(token.type)) return; var innerMode = CodeMirror.innerMode(editor.getMode(), token.state); if (innerMode.mode.helperType === "json") return; token.state = innerMode.state; // If it's not a 'word-style' token, ignore the token. if (!/^[\w$_]*$/.test(token.string)) { token = {start: cur.ch, end: cur.ch, string: "", state: token.state, type: token.string == "." ? "property" : null}; } else if (token.end > cur.ch) { token.end = cur.ch; token.string = token.string.slice(0, cur.ch - token.start); } var tprop = token; // If it is a property, find out what it is a property of. while (tprop.type == "property") { tprop = getToken(editor, Pos(cur.line, tprop.start)); if (tprop.string != ".") return; tprop = getToken(editor, Pos(cur.line, tprop.start)); if (!context) var context = []; context.push(tprop); } return {list: getCompletions(token, context, keywords, options), from: Pos(cur.line, token.start), to: Pos(cur.line, token.end)}; } function javascriptHint(editor, options) { return scriptHint(editor, javascriptKeywords, function (e, cur) {return e.getTokenAt(cur);}, options); } CodeMirror.registerHelper("hint", "javascript", javascriptHint); function getCoffeeScriptToken(editor, cur) { // This getToken, it is for coffeescript, imitates the behavior of // getTokenAt method in javascript.js, that is, returning "property" // type and treat "." as independent token. var token = editor.getTokenAt(cur); if (cur.ch == token.start + 1 && token.string.charAt(0) == '.') { token.end = token.start; token.string = '.'; token.type = "property"; } else if (/^\.[\w$_]*$/.test(token.string)) { token.type = "property"; token.start++; token.string = token.string.replace(/\./, ''); } return token; } function coffeescriptHint(editor, options) { return scriptHint(editor, coffeescriptKeywords, getCoffeeScriptToken, options); } CodeMirror.registerHelper("hint", "coffeescript", coffeescriptHint); var stringProps = ("charAt charCodeAt indexOf lastIndexOf substring substr slice trim trimLeft trimRight " + "toUpperCase toLowerCase split concat match replace search").split(" "); var arrayProps = ("length concat join splice push pop shift unshift slice reverse sort indexOf " + "lastIndexOf every some filter forEach map reduce reduceRight ").split(" "); var funcProps = "prototype apply call bind".split(" "); var javascriptKeywords = ("break case catch class const continue debugger default delete do else export extends false finally for function " + "if in import instanceof new null return super switch this throw true try typeof var void while with yield").split(" "); var coffeescriptKeywords = ("and break catch class continue delete do else extends false finally for " + "if in instanceof isnt new no not null of off on or return switch then throw true try typeof until void while with yes").split(" "); function forAllProps(obj, callback) { if (!Object.getOwnPropertyNames || !Object.getPrototypeOf) { for (var name in obj) callback(name) } else { for (var o = obj; o; o = Object.getPrototypeOf(o)) Object.getOwnPropertyNames(o).forEach(callback) } } function getCompletions(token, context, keywords, options) { var found = [], start = token.string, global = options && options.globalScope || window; function maybeAdd(str) { if (str.lastIndexOf(start, 0) == 0 && !arrayContains(found, str)) found.push(str); } function gatherCompletions(obj) { if (typeof obj == "string") forEach(stringProps, maybeAdd); else if (obj instanceof Array) forEach(arrayProps, maybeAdd); else if (obj instanceof Function) forEach(funcProps, maybeAdd); forAllProps(obj, maybeAdd) } if (context && context.length) { // If this is a property, see if it belongs to some object we can // find in the current environment. var obj = context.pop(), base; if (obj.type && obj.type.indexOf("variable") === 0) { if (options && options.additionalContext) base = options.additionalContext[obj.string]; if (!options || options.useGlobalScope !== false) base = base || global[obj.string]; } else if (obj.type == "string") { base = ""; } else if (obj.type == "atom") { base = 1; } else if (obj.type == "function") { if (global.jQuery != null && (obj.string == '$' || obj.string == 'jQuery') && (typeof global.jQuery == 'function')) base = global.jQuery(); else if (global._ != null && (obj.string == '_') && (typeof global._ == 'function')) base = global._(); } while (base != null && context.length) base = base[context.pop().string]; if (base != null) gatherCompletions(base); } else { // If not, just look in the global object, any local scope, and optional additional-context // (reading into JS mode internals to get at the local and global variables) for (var v = token.state.localVars; v; v = v.next) maybeAdd(v.name); for (var c = token.state.context; c; c = c.prev) for (var v = c.vars; v; v = v.next) maybeAdd(v.name) for (var v = token.state.globalVars; v; v = v.next) maybeAdd(v.name); if (options && options.additionalContext != null) for (var key in options.additionalContext) maybeAdd(key); if (!options || options.useGlobalScope !== false) gatherCompletions(global); forEach(keywords, maybeAdd); } return found; } }); ================================================ FILE: web/assets/codemirror/javascript.js ================================================ // CodeMirror, copyright (c) by Marijn Haverbeke and others // Distributed under an MIT license: https://codemirror.net/5/LICENSE (function(mod) { if (typeof exports == "object" && typeof module == "object") // CommonJS mod(require("../../lib/codemirror")); else if (typeof define == "function" && define.amd) // AMD define(["../../lib/codemirror"], mod); else // Plain browser env mod(CodeMirror); })(function(CodeMirror) { "use strict"; CodeMirror.defineMode("javascript", function(config, parserConfig) { var indentUnit = config.indentUnit; var statementIndent = parserConfig.statementIndent; var jsonldMode = parserConfig.jsonld; var jsonMode = parserConfig.json || jsonldMode; var trackScope = parserConfig.trackScope !== false var isTS = parserConfig.typescript; var wordRE = parserConfig.wordCharacters || /[\w$\xa1-\uffff]/; // Tokenizer var keywords = function(){ function kw(type) {return {type: type, style: "keyword"};} var A = kw("keyword a"), B = kw("keyword b"), C = kw("keyword c"), D = kw("keyword d"); var operator = kw("operator"), atom = {type: "atom", style: "atom"}; return { "if": kw("if"), "while": A, "with": A, "else": B, "do": B, "try": B, "finally": B, "return": D, "break": D, "continue": D, "new": kw("new"), "delete": C, "void": C, "throw": C, "debugger": kw("debugger"), "var": kw("var"), "const": kw("var"), "let": kw("var"), "function": kw("function"), "catch": kw("catch"), "for": kw("for"), "switch": kw("switch"), "case": kw("case"), "default": kw("default"), "in": operator, "typeof": operator, "instanceof": operator, "true": atom, "false": atom, "null": atom, "undefined": atom, "NaN": atom, "Infinity": atom, "this": kw("this"), "class": kw("class"), "super": kw("atom"), "yield": C, "export": kw("export"), "import": kw("import"), "extends": C, "await": C }; }(); var isOperatorChar = /[+\-*&%=<>!?|~^@]/; var isJsonldKeyword = /^@(context|id|value|language|type|container|list|set|reverse|index|base|vocab|graph)"/; function readRegexp(stream) { var escaped = false, next, inSet = false; while ((next = stream.next()) != null) { if (!escaped) { if (next == "/" && !inSet) return; if (next == "[") inSet = true; else if (inSet && next == "]") inSet = false; } escaped = !escaped && next == "\\"; } } // Used as scratch variables to communicate multiple values without // consing up tons of objects. var type, content; function ret(tp, style, cont) { type = tp; content = cont; return style; } function tokenBase(stream, state) { var ch = stream.next(); if (ch == '"' || ch == "'") { state.tokenize = tokenString(ch); return state.tokenize(stream, state); } else if (ch == "." && stream.match(/^\d[\d_]*(?:[eE][+\-]?[\d_]+)?/)) { return ret("number", "number"); } else if (ch == "." && stream.match("..")) { return ret("spread", "meta"); } else if (/[\[\]{}\(\),;\:\.]/.test(ch)) { return ret(ch); } else if (ch == "=" && stream.eat(">")) { return ret("=>", "operator"); } else if (ch == "0" && stream.match(/^(?:x[\dA-Fa-f_]+|o[0-7_]+|b[01_]+)n?/)) { return ret("number", "number"); } else if (/\d/.test(ch)) { stream.match(/^[\d_]*(?:n|(?:\.[\d_]*)?(?:[eE][+\-]?[\d_]+)?)?/); return ret("number", "number"); } else if (ch == "/") { if (stream.eat("*")) { state.tokenize = tokenComment; return tokenComment(stream, state); } else if (stream.eat("/")) { stream.skipToEnd(); return ret("comment", "comment"); } else if (expressionAllowed(stream, state, 1)) { readRegexp(stream); stream.match(/^\b(([gimyus])(?![gimyus]*\2))+\b/); return ret("regexp", "string-2"); } else { stream.eat("="); return ret("operator", "operator", stream.current()); } } else if (ch == "`") { state.tokenize = tokenQuasi; return tokenQuasi(stream, state); } else if (ch == "#" && stream.peek() == "!") { stream.skipToEnd(); return ret("meta", "meta"); } else if (ch == "#" && stream.eatWhile(wordRE)) { return ret("variable", "property") } else if (ch == "<" && stream.match("!--") || (ch == "-" && stream.match("->") && !/\S/.test(stream.string.slice(0, stream.start)))) { stream.skipToEnd() return ret("comment", "comment") } else if (isOperatorChar.test(ch)) { if (ch != ">" || !state.lexical || state.lexical.type != ">") { if (stream.eat("=")) { if (ch == "!" || ch == "=") stream.eat("=") } else if (/[<>*+\-|&?]/.test(ch)) { stream.eat(ch) if (ch == ">") stream.eat(ch) } } if (ch == "?" && stream.eat(".")) return ret(".") return ret("operator", "operator", stream.current()); } else if (wordRE.test(ch)) { stream.eatWhile(wordRE); var word = stream.current() if (state.lastType != ".") { if (keywords.propertyIsEnumerable(word)) { var kw = keywords[word] return ret(kw.type, kw.style, word) } if (word == "async" && stream.match(/^(\s|\/\*([^*]|\*(?!\/))*?\*\/)*[\[\(\w]/, false)) return ret("async", "keyword", word) } return ret("variable", "variable", word) } } function tokenString(quote) { return function(stream, state) { var escaped = false, next; if (jsonldMode && stream.peek() == "@" && stream.match(isJsonldKeyword)){ state.tokenize = tokenBase; return ret("jsonld-keyword", "meta"); } while ((next = stream.next()) != null) { if (next == quote && !escaped) break; escaped = !escaped && next == "\\"; } if (!escaped) state.tokenize = tokenBase; return ret("string", "string"); }; } function tokenComment(stream, state) { var maybeEnd = false, ch; while (ch = stream.next()) { if (ch == "/" && maybeEnd) { state.tokenize = tokenBase; break; } maybeEnd = (ch == "*"); } return ret("comment", "comment"); } function tokenQuasi(stream, state) { var escaped = false, next; while ((next = stream.next()) != null) { if (!escaped && (next == "`" || next == "$" && stream.eat("{"))) { state.tokenize = tokenBase; break; } escaped = !escaped && next == "\\"; } return ret("quasi", "string-2", stream.current()); } var brackets = "([{}])"; // This is a crude lookahead trick to try and notice that we're // parsing the argument patterns for a fat-arrow function before we // actually hit the arrow token. It only works if the arrow is on // the same line as the arguments and there's no strange noise // (comments) in between. Fallback is to only notice when we hit the // arrow, and not declare the arguments as locals for the arrow // body. function findFatArrow(stream, state) { if (state.fatArrowAt) state.fatArrowAt = null; var arrow = stream.string.indexOf("=>", stream.start); if (arrow < 0) return; if (isTS) { // Try to skip TypeScript return type declarations after the arguments var m = /:\s*(?:\w+(?:<[^>]*>|\[\])?|\{[^}]*\})\s*$/.exec(stream.string.slice(stream.start, arrow)) if (m) arrow = m.index } var depth = 0, sawSomething = false; for (var pos = arrow - 1; pos >= 0; --pos) { var ch = stream.string.charAt(pos); var bracket = brackets.indexOf(ch); if (bracket >= 0 && bracket < 3) { if (!depth) { ++pos; break; } if (--depth == 0) { if (ch == "(") sawSomething = true; break; } } else if (bracket >= 3 && bracket < 6) { ++depth; } else if (wordRE.test(ch)) { sawSomething = true; } else if (/["'\/`]/.test(ch)) { for (;; --pos) { if (pos == 0) return var next = stream.string.charAt(pos - 1) if (next == ch && stream.string.charAt(pos - 2) != "\\") { pos--; break } } } else if (sawSomething && !depth) { ++pos; break; } } if (sawSomething && !depth) state.fatArrowAt = pos; } // Parser var atomicTypes = {"atom": true, "number": true, "variable": true, "string": true, "regexp": true, "this": true, "import": true, "jsonld-keyword": true}; function JSLexical(indented, column, type, align, prev, info) { this.indented = indented; this.column = column; this.type = type; this.prev = prev; this.info = info; if (align != null) this.align = align; } function inScope(state, varname) { if (!trackScope) return false for (var v = state.localVars; v; v = v.next) if (v.name == varname) return true; for (var cx = state.context; cx; cx = cx.prev) { for (var v = cx.vars; v; v = v.next) if (v.name == varname) return true; } } function parseJS(state, style, type, content, stream) { var cc = state.cc; // Communicate our context to the combinators. // (Less wasteful than consing up a hundred closures on every call.) cx.state = state; cx.stream = stream; cx.marked = null, cx.cc = cc; cx.style = style; if (!state.lexical.hasOwnProperty("align")) state.lexical.align = true; while(true) { var combinator = cc.length ? cc.pop() : jsonMode ? expression : statement; if (combinator(type, content)) { while(cc.length && cc[cc.length - 1].lex) cc.pop()(); if (cx.marked) return cx.marked; if (type == "variable" && inScope(state, content)) return "variable-2"; return style; } } } // Combinator utils var cx = {state: null, column: null, marked: null, cc: null}; function pass() { for (var i = arguments.length - 1; i >= 0; i--) cx.cc.push(arguments[i]); } function cont() { pass.apply(null, arguments); return true; } function inList(name, list) { for (var v = list; v; v = v.next) if (v.name == name) return true return false; } function register(varname) { var state = cx.state; cx.marked = "def"; if (!trackScope) return if (state.context) { if (state.lexical.info == "var" && state.context && state.context.block) { // FIXME function decls are also not block scoped var newContext = registerVarScoped(varname, state.context) if (newContext != null) { state.context = newContext return } } else if (!inList(varname, state.localVars)) { state.localVars = new Var(varname, state.localVars) return } } // Fall through means this is global if (parserConfig.globalVars && !inList(varname, state.globalVars)) state.globalVars = new Var(varname, state.globalVars) } function registerVarScoped(varname, context) { if (!context) { return null } else if (context.block) { var inner = registerVarScoped(varname, context.prev) if (!inner) return null if (inner == context.prev) return context return new Context(inner, context.vars, true) } else if (inList(varname, context.vars)) { return context } else { return new Context(context.prev, new Var(varname, context.vars), false) } } function isModifier(name) { return name == "public" || name == "private" || name == "protected" || name == "abstract" || name == "readonly" } // Combinators function Context(prev, vars, block) { this.prev = prev; this.vars = vars; this.block = block } function Var(name, next) { this.name = name; this.next = next } var defaultVars = new Var("this", new Var("arguments", null)) function pushcontext() { cx.state.context = new Context(cx.state.context, cx.state.localVars, false) cx.state.localVars = defaultVars } function pushblockcontext() { cx.state.context = new Context(cx.state.context, cx.state.localVars, true) cx.state.localVars = null } pushcontext.lex = pushblockcontext.lex = true function popcontext() { cx.state.localVars = cx.state.context.vars cx.state.context = cx.state.context.prev } popcontext.lex = true function pushlex(type, info) { var result = function() { var state = cx.state, indent = state.indented; if (state.lexical.type == "stat") indent = state.lexical.indented; else for (var outer = state.lexical; outer && outer.type == ")" && outer.align; outer = outer.prev) indent = outer.indented; state.lexical = new JSLexical(indent, cx.stream.column(), type, null, state.lexical, info); }; result.lex = true; return result; } function poplex() { var state = cx.state; if (state.lexical.prev) { if (state.lexical.type == ")") state.indented = state.lexical.indented; state.lexical = state.lexical.prev; } } poplex.lex = true; function expect(wanted) { function exp(type) { if (type == wanted) return cont(); else if (wanted == ";" || type == "}" || type == ")" || type == "]") return pass(); else return cont(exp); } return exp; } function statement(type, value) { if (type == "var") return cont(pushlex("vardef", value), vardef, expect(";"), poplex); if (type == "keyword a") return cont(pushlex("form"), parenExpr, statement, poplex); if (type == "keyword b") return cont(pushlex("form"), statement, poplex); if (type == "keyword d") return cx.stream.match(/^\s*$/, false) ? cont() : cont(pushlex("stat"), maybeexpression, expect(";"), poplex); if (type == "debugger") return cont(expect(";")); if (type == "{") return cont(pushlex("}"), pushblockcontext, block, poplex, popcontext); if (type == ";") return cont(); if (type == "if") { if (cx.state.lexical.info == "else" && cx.state.cc[cx.state.cc.length - 1] == poplex) cx.state.cc.pop()(); return cont(pushlex("form"), parenExpr, statement, poplex, maybeelse); } if (type == "function") return cont(functiondef); if (type == "for") return cont(pushlex("form"), pushblockcontext, forspec, statement, popcontext, poplex); if (type == "class" || (isTS && value == "interface")) { cx.marked = "keyword" return cont(pushlex("form", type == "class" ? type : value), className, poplex) } if (type == "variable") { if (isTS && value == "declare") { cx.marked = "keyword" return cont(statement) } else if (isTS && (value == "module" || value == "enum" || value == "type") && cx.stream.match(/^\s*\w/, false)) { cx.marked = "keyword" if (value == "enum") return cont(enumdef); else if (value == "type") return cont(typename, expect("operator"), typeexpr, expect(";")); else return cont(pushlex("form"), pattern, expect("{"), pushlex("}"), block, poplex, poplex) } else if (isTS && value == "namespace") { cx.marked = "keyword" return cont(pushlex("form"), expression, statement, poplex) } else if (isTS && value == "abstract") { cx.marked = "keyword" return cont(statement) } else { return cont(pushlex("stat"), maybelabel); } } if (type == "switch") return cont(pushlex("form"), parenExpr, expect("{"), pushlex("}", "switch"), pushblockcontext, block, poplex, poplex, popcontext); if (type == "case") return cont(expression, expect(":")); if (type == "default") return cont(expect(":")); if (type == "catch") return cont(pushlex("form"), pushcontext, maybeCatchBinding, statement, poplex, popcontext); if (type == "export") return cont(pushlex("stat"), afterExport, poplex); if (type == "import") return cont(pushlex("stat"), afterImport, poplex); if (type == "async") return cont(statement) if (value == "@") return cont(expression, statement) return pass(pushlex("stat"), expression, expect(";"), poplex); } function maybeCatchBinding(type) { if (type == "(") return cont(funarg, expect(")")) } function expression(type, value) { return expressionInner(type, value, false); } function expressionNoComma(type, value) { return expressionInner(type, value, true); } function parenExpr(type) { if (type != "(") return pass() return cont(pushlex(")"), maybeexpression, expect(")"), poplex) } function expressionInner(type, value, noComma) { if (cx.state.fatArrowAt == cx.stream.start) { var body = noComma ? arrowBodyNoComma : arrowBody; if (type == "(") return cont(pushcontext, pushlex(")"), commasep(funarg, ")"), poplex, expect("=>"), body, popcontext); else if (type == "variable") return pass(pushcontext, pattern, expect("=>"), body, popcontext); } var maybeop = noComma ? maybeoperatorNoComma : maybeoperatorComma; if (atomicTypes.hasOwnProperty(type)) return cont(maybeop); if (type == "function") return cont(functiondef, maybeop); if (type == "class" || (isTS && value == "interface")) { cx.marked = "keyword"; return cont(pushlex("form"), classExpression, poplex); } if (type == "keyword c" || type == "async") return cont(noComma ? expressionNoComma : expression); if (type == "(") return cont(pushlex(")"), maybeexpression, expect(")"), poplex, maybeop); if (type == "operator" || type == "spread") return cont(noComma ? expressionNoComma : expression); if (type == "[") return cont(pushlex("]"), arrayLiteral, poplex, maybeop); if (type == "{") return contCommasep(objprop, "}", null, maybeop); if (type == "quasi") return pass(quasi, maybeop); if (type == "new") return cont(maybeTarget(noComma)); return cont(); } function maybeexpression(type) { if (type.match(/[;\}\)\],]/)) return pass(); return pass(expression); } function maybeoperatorComma(type, value) { if (type == ",") return cont(maybeexpression); return maybeoperatorNoComma(type, value, false); } function maybeoperatorNoComma(type, value, noComma) { var me = noComma == false ? maybeoperatorComma : maybeoperatorNoComma; var expr = noComma == false ? expression : expressionNoComma; if (type == "=>") return cont(pushcontext, noComma ? arrowBodyNoComma : arrowBody, popcontext); if (type == "operator") { if (/\+\+|--/.test(value) || isTS && value == "!") return cont(me); if (isTS && value == "<" && cx.stream.match(/^([^<>]|<[^<>]*>)*>\s*\(/, false)) return cont(pushlex(">"), commasep(typeexpr, ">"), poplex, me); if (value == "?") return cont(expression, expect(":"), expr); return cont(expr); } if (type == "quasi") { return pass(quasi, me); } if (type == ";") return; if (type == "(") return contCommasep(expressionNoComma, ")", "call", me); if (type == ".") return cont(property, me); if (type == "[") return cont(pushlex("]"), maybeexpression, expect("]"), poplex, me); if (isTS && value == "as") { cx.marked = "keyword"; return cont(typeexpr, me) } if (type == "regexp") { cx.state.lastType = cx.marked = "operator" cx.stream.backUp(cx.stream.pos - cx.stream.start - 1) return cont(expr) } } function quasi(type, value) { if (type != "quasi") return pass(); if (value.slice(value.length - 2) != "${") return cont(quasi); return cont(maybeexpression, continueQuasi); } function continueQuasi(type) { if (type == "}") { cx.marked = "string-2"; cx.state.tokenize = tokenQuasi; return cont(quasi); } } function arrowBody(type) { findFatArrow(cx.stream, cx.state); return pass(type == "{" ? statement : expression); } function arrowBodyNoComma(type) { findFatArrow(cx.stream, cx.state); return pass(type == "{" ? statement : expressionNoComma); } function maybeTarget(noComma) { return function(type) { if (type == ".") return cont(noComma ? targetNoComma : target); else if (type == "variable" && isTS) return cont(maybeTypeArgs, noComma ? maybeoperatorNoComma : maybeoperatorComma) else return pass(noComma ? expressionNoComma : expression); }; } function target(_, value) { if (value == "target") { cx.marked = "keyword"; return cont(maybeoperatorComma); } } function targetNoComma(_, value) { if (value == "target") { cx.marked = "keyword"; return cont(maybeoperatorNoComma); } } function maybelabel(type) { if (type == ":") return cont(poplex, statement); return pass(maybeoperatorComma, expect(";"), poplex); } function property(type) { if (type == "variable") {cx.marked = "property"; return cont();} } function objprop(type, value) { if (type == "async") { cx.marked = "property"; return cont(objprop); } else if (type == "variable" || cx.style == "keyword") { cx.marked = "property"; if (value == "get" || value == "set") return cont(getterSetter); var m // Work around fat-arrow-detection complication for detecting typescript typed arrow params if (isTS && cx.state.fatArrowAt == cx.stream.start && (m = cx.stream.match(/^\s*:\s*/, false))) cx.state.fatArrowAt = cx.stream.pos + m[0].length return cont(afterprop); } else if (type == "number" || type == "string") { cx.marked = jsonldMode ? "property" : (cx.style + " property"); return cont(afterprop); } else if (type == "jsonld-keyword") { return cont(afterprop); } else if (isTS && isModifier(value)) { cx.marked = "keyword" return cont(objprop) } else if (type == "[") { return cont(expression, maybetype, expect("]"), afterprop); } else if (type == "spread") { return cont(expressionNoComma, afterprop); } else if (value == "*") { cx.marked = "keyword"; return cont(objprop); } else if (type == ":") { return pass(afterprop) } } function getterSetter(type) { if (type != "variable") return pass(afterprop); cx.marked = "property"; return cont(functiondef); } function afterprop(type) { if (type == ":") return cont(expressionNoComma); if (type == "(") return pass(functiondef); } function commasep(what, end, sep) { function proceed(type, value) { if (sep ? sep.indexOf(type) > -1 : type == ",") { var lex = cx.state.lexical; if (lex.info == "call") lex.pos = (lex.pos || 0) + 1; return cont(function(type, value) { if (type == end || value == end) return pass() return pass(what) }, proceed); } if (type == end || value == end) return cont(); if (sep && sep.indexOf(";") > -1) return pass(what) return cont(expect(end)); } return function(type, value) { if (type == end || value == end) return cont(); return pass(what, proceed); }; } function contCommasep(what, end, info) { for (var i = 3; i < arguments.length; i++) cx.cc.push(arguments[i]); return cont(pushlex(end, info), commasep(what, end), poplex); } function block(type) { if (type == "}") return cont(); return pass(statement, block); } function maybetype(type, value) { if (isTS) { if (type == ":") return cont(typeexpr); if (value == "?") return cont(maybetype); } } function maybetypeOrIn(type, value) { if (isTS && (type == ":" || value == "in")) return cont(typeexpr) } function mayberettype(type) { if (isTS && type == ":") { if (cx.stream.match(/^\s*\w+\s+is\b/, false)) return cont(expression, isKW, typeexpr) else return cont(typeexpr) } } function isKW(_, value) { if (value == "is") { cx.marked = "keyword" return cont() } } function typeexpr(type, value) { if (value == "keyof" || value == "typeof" || value == "infer" || value == "readonly") { cx.marked = "keyword" return cont(value == "typeof" ? expressionNoComma : typeexpr) } if (type == "variable" || value == "void") { cx.marked = "type" return cont(afterType) } if (value == "|" || value == "&") return cont(typeexpr) if (type == "string" || type == "number" || type == "atom") return cont(afterType); if (type == "[") return cont(pushlex("]"), commasep(typeexpr, "]", ","), poplex, afterType) if (type == "{") return cont(pushlex("}"), typeprops, poplex, afterType) if (type == "(") return cont(commasep(typearg, ")"), maybeReturnType, afterType) if (type == "<") return cont(commasep(typeexpr, ">"), typeexpr) if (type == "quasi") { return pass(quasiType, afterType); } } function maybeReturnType(type) { if (type == "=>") return cont(typeexpr) } function typeprops(type) { if (type.match(/[\}\)\]]/)) return cont() if (type == "," || type == ";") return cont(typeprops) return pass(typeprop, typeprops) } function typeprop(type, value) { if (type == "variable" || cx.style == "keyword") { cx.marked = "property" return cont(typeprop) } else if (value == "?" || type == "number" || type == "string") { return cont(typeprop) } else if (type == ":") { return cont(typeexpr) } else if (type == "[") { return cont(expect("variable"), maybetypeOrIn, expect("]"), typeprop) } else if (type == "(") { return pass(functiondecl, typeprop) } else if (!type.match(/[;\}\)\],]/)) { return cont() } } function quasiType(type, value) { if (type != "quasi") return pass(); if (value.slice(value.length - 2) != "${") return cont(quasiType); return cont(typeexpr, continueQuasiType); } function continueQuasiType(type) { if (type == "}") { cx.marked = "string-2"; cx.state.tokenize = tokenQuasi; return cont(quasiType); } } function typearg(type, value) { if (type == "variable" && cx.stream.match(/^\s*[?:]/, false) || value == "?") return cont(typearg) if (type == ":") return cont(typeexpr) if (type == "spread") return cont(typearg) return pass(typeexpr) } function afterType(type, value) { if (value == "<") return cont(pushlex(">"), commasep(typeexpr, ">"), poplex, afterType) if (value == "|" || type == "." || value == "&") return cont(typeexpr) if (type == "[") return cont(typeexpr, expect("]"), afterType) if (value == "extends" || value == "implements") { cx.marked = "keyword"; return cont(typeexpr) } if (value == "?") return cont(typeexpr, expect(":"), typeexpr) } function maybeTypeArgs(_, value) { if (value == "<") return cont(pushlex(">"), commasep(typeexpr, ">"), poplex, afterType) } function typeparam() { return pass(typeexpr, maybeTypeDefault) } function maybeTypeDefault(_, value) { if (value == "=") return cont(typeexpr) } function vardef(_, value) { if (value == "enum") {cx.marked = "keyword"; return cont(enumdef)} return pass(pattern, maybetype, maybeAssign, vardefCont); } function pattern(type, value) { if (isTS && isModifier(value)) { cx.marked = "keyword"; return cont(pattern) } if (type == "variable") { register(value); return cont(); } if (type == "spread") return cont(pattern); if (type == "[") return contCommasep(eltpattern, "]"); if (type == "{") return contCommasep(proppattern, "}"); } function proppattern(type, value) { if (type == "variable" && !cx.stream.match(/^\s*:/, false)) { register(value); return cont(maybeAssign); } if (type == "variable") cx.marked = "property"; if (type == "spread") return cont(pattern); if (type == "}") return pass(); if (type == "[") return cont(expression, expect(']'), expect(':'), proppattern); return cont(expect(":"), pattern, maybeAssign); } function eltpattern() { return pass(pattern, maybeAssign) } function maybeAssign(_type, value) { if (value == "=") return cont(expressionNoComma); } function vardefCont(type) { if (type == ",") return cont(vardef); } function maybeelse(type, value) { if (type == "keyword b" && value == "else") return cont(pushlex("form", "else"), statement, poplex); } function forspec(type, value) { if (value == "await") return cont(forspec); if (type == "(") return cont(pushlex(")"), forspec1, poplex); } function forspec1(type) { if (type == "var") return cont(vardef, forspec2); if (type == "variable") return cont(forspec2); return pass(forspec2) } function forspec2(type, value) { if (type == ")") return cont() if (type == ";") return cont(forspec2) if (value == "in" || value == "of") { cx.marked = "keyword"; return cont(expression, forspec2) } return pass(expression, forspec2) } function functiondef(type, value) { if (value == "*") {cx.marked = "keyword"; return cont(functiondef);} if (type == "variable") {register(value); return cont(functiondef);} if (type == "(") return cont(pushcontext, pushlex(")"), commasep(funarg, ")"), poplex, mayberettype, statement, popcontext); if (isTS && value == "<") return cont(pushlex(">"), commasep(typeparam, ">"), poplex, functiondef) } function functiondecl(type, value) { if (value == "*") {cx.marked = "keyword"; return cont(functiondecl);} if (type == "variable") {register(value); return cont(functiondecl);} if (type == "(") return cont(pushcontext, pushlex(")"), commasep(funarg, ")"), poplex, mayberettype, popcontext); if (isTS && value == "<") return cont(pushlex(">"), commasep(typeparam, ">"), poplex, functiondecl) } function typename(type, value) { if (type == "keyword" || type == "variable") { cx.marked = "type" return cont(typename) } else if (value == "<") { return cont(pushlex(">"), commasep(typeparam, ">"), poplex) } } function funarg(type, value) { if (value == "@") cont(expression, funarg) if (type == "spread") return cont(funarg); if (isTS && isModifier(value)) { cx.marked = "keyword"; return cont(funarg); } if (isTS && type == "this") return cont(maybetype, maybeAssign) return pass(pattern, maybetype, maybeAssign); } function classExpression(type, value) { // Class expressions may have an optional name. if (type == "variable") return className(type, value); return classNameAfter(type, value); } function className(type, value) { if (type == "variable") {register(value); return cont(classNameAfter);} } function classNameAfter(type, value) { if (value == "<") return cont(pushlex(">"), commasep(typeparam, ">"), poplex, classNameAfter) if (value == "extends" || value == "implements" || (isTS && type == ",")) { if (value == "implements") cx.marked = "keyword"; return cont(isTS ? typeexpr : expression, classNameAfter); } if (type == "{") return cont(pushlex("}"), classBody, poplex); } function classBody(type, value) { if (type == "async" || (type == "variable" && (value == "static" || value == "get" || value == "set" || (isTS && isModifier(value))) && cx.stream.match(/^\s+#?[\w$\xa1-\uffff]/, false))) { cx.marked = "keyword"; return cont(classBody); } if (type == "variable" || cx.style == "keyword") { cx.marked = "property"; return cont(classfield, classBody); } if (type == "number" || type == "string") return cont(classfield, classBody); if (type == "[") return cont(expression, maybetype, expect("]"), classfield, classBody) if (value == "*") { cx.marked = "keyword"; return cont(classBody); } if (isTS && type == "(") return pass(functiondecl, classBody) if (type == ";" || type == ",") return cont(classBody); if (type == "}") return cont(); if (value == "@") return cont(expression, classBody) } function classfield(type, value) { if (value == "!") return cont(classfield) if (value == "?") return cont(classfield) if (type == ":") return cont(typeexpr, maybeAssign) if (value == "=") return cont(expressionNoComma) var context = cx.state.lexical.prev, isInterface = context && context.info == "interface" return pass(isInterface ? functiondecl : functiondef) } function afterExport(type, value) { if (value == "*") { cx.marked = "keyword"; return cont(maybeFrom, expect(";")); } if (value == "default") { cx.marked = "keyword"; return cont(expression, expect(";")); } if (type == "{") return cont(commasep(exportField, "}"), maybeFrom, expect(";")); return pass(statement); } function exportField(type, value) { if (value == "as") { cx.marked = "keyword"; return cont(expect("variable")); } if (type == "variable") return pass(expressionNoComma, exportField); } function afterImport(type) { if (type == "string") return cont(); if (type == "(") return pass(expression); if (type == ".") return pass(maybeoperatorComma); return pass(importSpec, maybeMoreImports, maybeFrom); } function importSpec(type, value) { if (type == "{") return contCommasep(importSpec, "}"); if (type == "variable") register(value); if (value == "*") cx.marked = "keyword"; return cont(maybeAs); } function maybeMoreImports(type) { if (type == ",") return cont(importSpec, maybeMoreImports) } function maybeAs(_type, value) { if (value == "as") { cx.marked = "keyword"; return cont(importSpec); } } function maybeFrom(_type, value) { if (value == "from") { cx.marked = "keyword"; return cont(expression); } } function arrayLiteral(type) { if (type == "]") return cont(); return pass(commasep(expressionNoComma, "]")); } function enumdef() { return pass(pushlex("form"), pattern, expect("{"), pushlex("}"), commasep(enummember, "}"), poplex, poplex) } function enummember() { return pass(pattern, maybeAssign); } function isContinuedStatement(state, textAfter) { return state.lastType == "operator" || state.lastType == "," || isOperatorChar.test(textAfter.charAt(0)) || /[,.]/.test(textAfter.charAt(0)); } function expressionAllowed(stream, state, backUp) { return state.tokenize == tokenBase && /^(?:operator|sof|keyword [bcd]|case|new|export|default|spread|[\[{}\(,;:]|=>)$/.test(state.lastType) || (state.lastType == "quasi" && /\{\s*$/.test(stream.string.slice(0, stream.pos - (backUp || 0)))) } // Interface return { startState: function(basecolumn) { var state = { tokenize: tokenBase, lastType: "sof", cc: [], lexical: new JSLexical((basecolumn || 0) - indentUnit, 0, "block", false), localVars: parserConfig.localVars, context: parserConfig.localVars && new Context(null, null, false), indented: basecolumn || 0 }; if (parserConfig.globalVars && typeof parserConfig.globalVars == "object") state.globalVars = parserConfig.globalVars; return state; }, token: function(stream, state) { if (stream.sol()) { if (!state.lexical.hasOwnProperty("align")) state.lexical.align = false; state.indented = stream.indentation(); findFatArrow(stream, state); } if (state.tokenize != tokenComment && stream.eatSpace()) return null; var style = state.tokenize(stream, state); if (type == "comment") return style; state.lastType = type == "operator" && (content == "++" || content == "--") ? "incdec" : type; return parseJS(state, style, type, content, stream); }, indent: function(state, textAfter) { if (state.tokenize == tokenComment || state.tokenize == tokenQuasi) return CodeMirror.Pass; if (state.tokenize != tokenBase) return 0; var firstChar = textAfter && textAfter.charAt(0), lexical = state.lexical, top // Kludge to prevent 'maybelse' from blocking lexical scope pops if (!/^\s*else\b/.test(textAfter)) for (var i = state.cc.length - 1; i >= 0; --i) { var c = state.cc[i]; if (c == poplex) lexical = lexical.prev; else if (c != maybeelse && c != popcontext) break; } while ((lexical.type == "stat" || lexical.type == "form") && (firstChar == "}" || ((top = state.cc[state.cc.length - 1]) && (top == maybeoperatorComma || top == maybeoperatorNoComma) && !/^[,\.=+\-*:?[\(]/.test(textAfter)))) lexical = lexical.prev; if (statementIndent && lexical.type == ")" && lexical.prev.type == "stat") lexical = lexical.prev; var type = lexical.type, closing = firstChar == type; if (type == "vardef") return lexical.indented + (state.lastType == "operator" || state.lastType == "," ? lexical.info.length + 1 : 0); else if (type == "form" && firstChar == "{") return lexical.indented; else if (type == "form") return lexical.indented + indentUnit; else if (type == "stat") return lexical.indented + (isContinuedStatement(state, textAfter) ? statementIndent || indentUnit : 0); else if (lexical.info == "switch" && !closing && parserConfig.doubleIndentSwitch != false) return lexical.indented + (/^(?:case|default)\b/.test(textAfter) ? indentUnit : 2 * indentUnit); else if (lexical.align) return lexical.column + (closing ? 0 : 1); else return lexical.indented + (closing ? 0 : indentUnit); }, electricInput: /^\s*(?:case .*?:|default:|\{|\})$/, blockCommentStart: jsonMode ? null : "/*", blockCommentEnd: jsonMode ? null : "*/", blockCommentContinue: jsonMode ? null : " * ", lineComment: jsonMode ? null : "//", fold: "brace", closeBrackets: "()[]{}''\"\"``", helperType: jsonMode ? "json" : "javascript", jsonldMode: jsonldMode, jsonMode: jsonMode, expressionAllowed: expressionAllowed, skipExpression: function(state) { parseJS(state, "atom", "atom", "true", new CodeMirror.StringStream("", 2, null)) } }; }); CodeMirror.registerHelper("wordChars", "javascript", /[\w$]/); CodeMirror.defineMIME("text/javascript", "javascript"); CodeMirror.defineMIME("text/ecmascript", "javascript"); CodeMirror.defineMIME("application/javascript", "javascript"); CodeMirror.defineMIME("application/x-javascript", "javascript"); CodeMirror.defineMIME("application/ecmascript", "javascript"); CodeMirror.defineMIME("application/json", { name: "javascript", json: true }); CodeMirror.defineMIME("application/x-json", { name: "javascript", json: true }); CodeMirror.defineMIME("application/manifest+json", { name: "javascript", json: true }) CodeMirror.defineMIME("application/ld+json", { name: "javascript", jsonld: true }); CodeMirror.defineMIME("text/typescript", { name: "javascript", typescript: true }); CodeMirror.defineMIME("application/typescript", { name: "javascript", typescript: true }); }); ================================================ FILE: web/assets/codemirror/jshint.js ================================================ /*! 2.13.2 */ var JSHINT; if (typeof window === 'undefined') window = {}; (function () { var require; require=(function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o= 65 && i <= 90 || // A-Z i === 95 || // _ i >= 97 && i <= 122; // a-z } var identifierPartTable = []; for (var i = 0; i < 128; i++) { identifierPartTable[i] = identifierStartTable[i] || // $, _, A-Z, a-z i >= 48 && i <= 57; // 0-9 } module.exports = { asciiIdentifierStartTable: identifierStartTable, asciiIdentifierPartTable: identifierPartTable }; },{}],2:[function(require,module,exports){ module.exports = /^(?:[\$A-Z_a-z\xAA\xB5\xBA\xC0-\xD6\xD8-\xF6\xF8-\u02C1\u02C6-\u02D1\u02E0-\u02E4\u02EC\u02EE\u0370-\u0374\u0376\u0377\u037A-\u037D\u0386\u0388-\u038A\u038C\u038E-\u03A1\u03A3-\u03F5\u03F7-\u0481\u048A-\u0525\u0531-\u0556\u0559\u0561-\u0587\u05D0-\u05EA\u05F0-\u05F2\u0621-\u064A\u066E\u066F\u0671-\u06D3\u06D5\u06E5\u06E6\u06EE\u06EF\u06FA-\u06FC\u06FF\u0710\u0712-\u072F\u074D-\u07A5\u07B1\u07CA-\u07EA\u07F4\u07F5\u07FA\u0800-\u0815\u081A\u0824\u0828\u0904-\u0939\u093D\u0950\u0958-\u0961\u0971\u0972\u0979-\u097F\u0985-\u098C\u098F\u0990\u0993-\u09A8\u09AA-\u09B0\u09B2\u09B6-\u09B9\u09BD\u09CE\u09DC\u09DD\u09DF-\u09E1\u09F0\u09F1\u0A05-\u0A0A\u0A0F\u0A10\u0A13-\u0A28\u0A2A-\u0A30\u0A32\u0A33\u0A35\u0A36\u0A38\u0A39\u0A59-\u0A5C\u0A5E\u0A72-\u0A74\u0A85-\u0A8D\u0A8F-\u0A91\u0A93-\u0AA8\u0AAA-\u0AB0\u0AB2\u0AB3\u0AB5-\u0AB9\u0ABD\u0AD0\u0AE0\u0AE1\u0B05-\u0B0C\u0B0F\u0B10\u0B13-\u0B28\u0B2A-\u0B30\u0B32\u0B33\u0B35-\u0B39\u0B3D\u0B5C\u0B5D\u0B5F-\u0B61\u0B71\u0B83\u0B85-\u0B8A\u0B8E-\u0B90\u0B92-\u0B95\u0B99\u0B9A\u0B9C\u0B9E\u0B9F\u0BA3\u0BA4\u0BA8-\u0BAA\u0BAE-\u0BB9\u0BD0\u0C05-\u0C0C\u0C0E-\u0C10\u0C12-\u0C28\u0C2A-\u0C33\u0C35-\u0C39\u0C3D\u0C58\u0C59\u0C60\u0C61\u0C85-\u0C8C\u0C8E-\u0C90\u0C92-\u0CA8\u0CAA-\u0CB3\u0CB5-\u0CB9\u0CBD\u0CDE\u0CE0\u0CE1\u0D05-\u0D0C\u0D0E-\u0D10\u0D12-\u0D28\u0D2A-\u0D39\u0D3D\u0D60\u0D61\u0D7A-\u0D7F\u0D85-\u0D96\u0D9A-\u0DB1\u0DB3-\u0DBB\u0DBD\u0DC0-\u0DC6\u0E01-\u0E30\u0E32\u0E33\u0E40-\u0E46\u0E81\u0E82\u0E84\u0E87\u0E88\u0E8A\u0E8D\u0E94-\u0E97\u0E99-\u0E9F\u0EA1-\u0EA3\u0EA5\u0EA7\u0EAA\u0EAB\u0EAD-\u0EB0\u0EB2\u0EB3\u0EBD\u0EC0-\u0EC4\u0EC6\u0EDC\u0EDD\u0F00\u0F40-\u0F47\u0F49-\u0F6C\u0F88-\u0F8B\u1000-\u102A\u103F\u1050-\u1055\u105A-\u105D\u1061\u1065\u1066\u106E-\u1070\u1075-\u1081\u108E\u10A0-\u10C5\u10D0-\u10FA\u10FC\u1100-\u1248\u124A-\u124D\u1250-\u1256\u1258\u125A-\u125D\u1260-\u1288\u128A-\u128D\u1290-\u12B0\u12B2-\u12B5\u12B8-\u12BE\u12C0\u12C2-\u12C5\u12C8-\u12D6\u12D8-\u1310\u1312-\u1315\u1318-\u135A\u1380-\u138F\u13A0-\u13F4\u1401-\u166C\u166F-\u167F\u1681-\u169A\u16A0-\u16EA\u16EE-\u16F0\u1700-\u170C\u170E-\u1711\u1720-\u1731\u1740-\u1751\u1760-\u176C\u176E-\u1770\u1780-\u17B3\u17D7\u17DC\u1820-\u1877\u1880-\u18A8\u18AA\u18B0-\u18F5\u1900-\u191C\u1950-\u196D\u1970-\u1974\u1980-\u19AB\u19C1-\u19C7\u1A00-\u1A16\u1A20-\u1A54\u1AA7\u1B05-\u1B33\u1B45-\u1B4B\u1B83-\u1BA0\u1BAE\u1BAF\u1C00-\u1C23\u1C4D-\u1C4F\u1C5A-\u1C7D\u1CE9-\u1CEC\u1CEE-\u1CF1\u1D00-\u1DBF\u1E00-\u1F15\u1F18-\u1F1D\u1F20-\u1F45\u1F48-\u1F4D\u1F50-\u1F57\u1F59\u1F5B\u1F5D\u1F5F-\u1F7D\u1F80-\u1FB4\u1FB6-\u1FBC\u1FBE\u1FC2-\u1FC4\u1FC6-\u1FCC\u1FD0-\u1FD3\u1FD6-\u1FDB\u1FE0-\u1FEC\u1FF2-\u1FF4\u1FF6-\u1FFC\u2071\u207F\u2090-\u2094\u2102\u2107\u210A-\u2113\u2115\u2119-\u211D\u2124\u2126\u2128\u212A-\u212D\u212F-\u2139\u213C-\u213F\u2145-\u2149\u214E\u2160-\u2188\u2C00-\u2C2E\u2C30-\u2C5E\u2C60-\u2CE4\u2CEB-\u2CEE\u2D00-\u2D25\u2D30-\u2D65\u2D6F\u2D80-\u2D96\u2DA0-\u2DA6\u2DA8-\u2DAE\u2DB0-\u2DB6\u2DB8-\u2DBE\u2DC0-\u2DC6\u2DC8-\u2DCE\u2DD0-\u2DD6\u2DD8-\u2DDE\u2E2F\u3005-\u3007\u3021-\u3029\u3031-\u3035\u3038-\u303C\u3041-\u3096\u309D-\u309F\u30A1-\u30FA\u30FC-\u30FF\u3105-\u312D\u3131-\u318E\u31A0-\u31B7\u31F0-\u31FF\u3400-\u4DB5\u4E00-\u9FCB\uA000-\uA48C\uA4D0-\uA4FD\uA500-\uA60C\uA610-\uA61F\uA62A\uA62B\uA640-\uA65F\uA662-\uA66E\uA67F-\uA697\uA6A0-\uA6EF\uA717-\uA71F\uA722-\uA788\uA78B\uA78C\uA7FB-\uA801\uA803-\uA805\uA807-\uA80A\uA80C-\uA822\uA840-\uA873\uA882-\uA8B3\uA8F2-\uA8F7\uA8FB\uA90A-\uA925\uA930-\uA946\uA960-\uA97C\uA984-\uA9B2\uA9CF\uAA00-\uAA28\uAA40-\uAA42\uAA44-\uAA4B\uAA60-\uAA76\uAA7A\uAA80-\uAAAF\uAAB1\uAAB5\uAAB6\uAAB9-\uAABD\uAAC0\uAAC2\uAADB-\uAADD\uABC0-\uABE2\uAC00-\uD7A3\uD7B0-\uD7C6\uD7CB-\uD7FB\uF900-\uFA2D\uFA30-\uFA6D\uFA70-\uFAD9\uFB00-\uFB06\uFB13-\uFB17\uFB1D\uFB1F-\uFB28\uFB2A-\uFB36\uFB38-\uFB3C\uFB3E\uFB40\uFB41\uFB43\uFB44\uFB46-\uFBB1\uFBD3-\uFD3D\uFD50-\uFD8F\uFD92-\uFDC7\uFDF0-\uFDFB\uFE70-\uFE74\uFE76-\uFEFC\uFF21-\uFF3A\uFF41-\uFF5A\uFF66-\uFFBE\uFFC2-\uFFC7\uFFCA-\uFFCF\uFFD2-\uFFD7\uFFDA-\uFFDC])(?:[\$0-9A-Z_a-z\xAA\xB5\xBA\xC0-\xD6\xD8-\xF6\xF8-\u02C1\u02C6-\u02D1\u02E0-\u02E4\u02EC\u02EE\u0300-\u0374\u0376\u0377\u037A-\u037D\u0386\u0388-\u038A\u038C\u038E-\u03A1\u03A3-\u03F5\u03F7-\u0481\u0483-\u0487\u048A-\u0525\u0531-\u0556\u0559\u0561-\u0587\u0591-\u05BD\u05BF\u05C1\u05C2\u05C4\u05C5\u05C7\u05D0-\u05EA\u05F0-\u05F2\u0610-\u061A\u0621-\u065E\u0660-\u0669\u066E-\u06D3\u06D5-\u06DC\u06DF-\u06E8\u06EA-\u06FC\u06FF\u0710-\u074A\u074D-\u07B1\u07C0-\u07F5\u07FA\u0800-\u082D\u0900-\u0939\u093C-\u094E\u0950-\u0955\u0958-\u0963\u0966-\u096F\u0971\u0972\u0979-\u097F\u0981-\u0983\u0985-\u098C\u098F\u0990\u0993-\u09A8\u09AA-\u09B0\u09B2\u09B6-\u09B9\u09BC-\u09C4\u09C7\u09C8\u09CB-\u09CE\u09D7\u09DC\u09DD\u09DF-\u09E3\u09E6-\u09F1\u0A01-\u0A03\u0A05-\u0A0A\u0A0F\u0A10\u0A13-\u0A28\u0A2A-\u0A30\u0A32\u0A33\u0A35\u0A36\u0A38\u0A39\u0A3C\u0A3E-\u0A42\u0A47\u0A48\u0A4B-\u0A4D\u0A51\u0A59-\u0A5C\u0A5E\u0A66-\u0A75\u0A81-\u0A83\u0A85-\u0A8D\u0A8F-\u0A91\u0A93-\u0AA8\u0AAA-\u0AB0\u0AB2\u0AB3\u0AB5-\u0AB9\u0ABC-\u0AC5\u0AC7-\u0AC9\u0ACB-\u0ACD\u0AD0\u0AE0-\u0AE3\u0AE6-\u0AEF\u0B01-\u0B03\u0B05-\u0B0C\u0B0F\u0B10\u0B13-\u0B28\u0B2A-\u0B30\u0B32\u0B33\u0B35-\u0B39\u0B3C-\u0B44\u0B47\u0B48\u0B4B-\u0B4D\u0B56\u0B57\u0B5C\u0B5D\u0B5F-\u0B63\u0B66-\u0B6F\u0B71\u0B82\u0B83\u0B85-\u0B8A\u0B8E-\u0B90\u0B92-\u0B95\u0B99\u0B9A\u0B9C\u0B9E\u0B9F\u0BA3\u0BA4\u0BA8-\u0BAA\u0BAE-\u0BB9\u0BBE-\u0BC2\u0BC6-\u0BC8\u0BCA-\u0BCD\u0BD0\u0BD7\u0BE6-\u0BEF\u0C01-\u0C03\u0C05-\u0C0C\u0C0E-\u0C10\u0C12-\u0C28\u0C2A-\u0C33\u0C35-\u0C39\u0C3D-\u0C44\u0C46-\u0C48\u0C4A-\u0C4D\u0C55\u0C56\u0C58\u0C59\u0C60-\u0C63\u0C66-\u0C6F\u0C82\u0C83\u0C85-\u0C8C\u0C8E-\u0C90\u0C92-\u0CA8\u0CAA-\u0CB3\u0CB5-\u0CB9\u0CBC-\u0CC4\u0CC6-\u0CC8\u0CCA-\u0CCD\u0CD5\u0CD6\u0CDE\u0CE0-\u0CE3\u0CE6-\u0CEF\u0D02\u0D03\u0D05-\u0D0C\u0D0E-\u0D10\u0D12-\u0D28\u0D2A-\u0D39\u0D3D-\u0D44\u0D46-\u0D48\u0D4A-\u0D4D\u0D57\u0D60-\u0D63\u0D66-\u0D6F\u0D7A-\u0D7F\u0D82\u0D83\u0D85-\u0D96\u0D9A-\u0DB1\u0DB3-\u0DBB\u0DBD\u0DC0-\u0DC6\u0DCA\u0DCF-\u0DD4\u0DD6\u0DD8-\u0DDF\u0DF2\u0DF3\u0E01-\u0E3A\u0E40-\u0E4E\u0E50-\u0E59\u0E81\u0E82\u0E84\u0E87\u0E88\u0E8A\u0E8D\u0E94-\u0E97\u0E99-\u0E9F\u0EA1-\u0EA3\u0EA5\u0EA7\u0EAA\u0EAB\u0EAD-\u0EB9\u0EBB-\u0EBD\u0EC0-\u0EC4\u0EC6\u0EC8-\u0ECD\u0ED0-\u0ED9\u0EDC\u0EDD\u0F00\u0F18\u0F19\u0F20-\u0F29\u0F35\u0F37\u0F39\u0F3E-\u0F47\u0F49-\u0F6C\u0F71-\u0F84\u0F86-\u0F8B\u0F90-\u0F97\u0F99-\u0FBC\u0FC6\u1000-\u1049\u1050-\u109D\u10A0-\u10C5\u10D0-\u10FA\u10FC\u1100-\u1248\u124A-\u124D\u1250-\u1256\u1258\u125A-\u125D\u1260-\u1288\u128A-\u128D\u1290-\u12B0\u12B2-\u12B5\u12B8-\u12BE\u12C0\u12C2-\u12C5\u12C8-\u12D6\u12D8-\u1310\u1312-\u1315\u1318-\u135A\u135F\u1380-\u138F\u13A0-\u13F4\u1401-\u166C\u166F-\u167F\u1681-\u169A\u16A0-\u16EA\u16EE-\u16F0\u1700-\u170C\u170E-\u1714\u1720-\u1734\u1740-\u1753\u1760-\u176C\u176E-\u1770\u1772\u1773\u1780-\u17B3\u17B6-\u17D3\u17D7\u17DC\u17DD\u17E0-\u17E9\u180B-\u180D\u1810-\u1819\u1820-\u1877\u1880-\u18AA\u18B0-\u18F5\u1900-\u191C\u1920-\u192B\u1930-\u193B\u1946-\u196D\u1970-\u1974\u1980-\u19AB\u19B0-\u19C9\u19D0-\u19DA\u1A00-\u1A1B\u1A20-\u1A5E\u1A60-\u1A7C\u1A7F-\u1A89\u1A90-\u1A99\u1AA7\u1B00-\u1B4B\u1B50-\u1B59\u1B6B-\u1B73\u1B80-\u1BAA\u1BAE-\u1BB9\u1C00-\u1C37\u1C40-\u1C49\u1C4D-\u1C7D\u1CD0-\u1CD2\u1CD4-\u1CF2\u1D00-\u1DE6\u1DFD-\u1F15\u1F18-\u1F1D\u1F20-\u1F45\u1F48-\u1F4D\u1F50-\u1F57\u1F59\u1F5B\u1F5D\u1F5F-\u1F7D\u1F80-\u1FB4\u1FB6-\u1FBC\u1FBE\u1FC2-\u1FC4\u1FC6-\u1FCC\u1FD0-\u1FD3\u1FD6-\u1FDB\u1FE0-\u1FEC\u1FF2-\u1FF4\u1FF6-\u1FFC\u200C\u200D\u203F\u2040\u2054\u2071\u207F\u2090-\u2094\u20D0-\u20DC\u20E1\u20E5-\u20F0\u2102\u2107\u210A-\u2113\u2115\u2119-\u211D\u2124\u2126\u2128\u212A-\u212D\u212F-\u2139\u213C-\u213F\u2145-\u2149\u214E\u2160-\u2188\u2C00-\u2C2E\u2C30-\u2C5E\u2C60-\u2CE4\u2CEB-\u2CF1\u2D00-\u2D25\u2D30-\u2D65\u2D6F\u2D80-\u2D96\u2DA0-\u2DA6\u2DA8-\u2DAE\u2DB0-\u2DB6\u2DB8-\u2DBE\u2DC0-\u2DC6\u2DC8-\u2DCE\u2DD0-\u2DD6\u2DD8-\u2DDE\u2DE0-\u2DFF\u2E2F\u3005-\u3007\u3021-\u302F\u3031-\u3035\u3038-\u303C\u3041-\u3096\u3099\u309A\u309D-\u309F\u30A1-\u30FA\u30FC-\u30FF\u3105-\u312D\u3131-\u318E\u31A0-\u31B7\u31F0-\u31FF\u3400-\u4DB5\u4E00-\u9FCB\uA000-\uA48C\uA4D0-\uA4FD\uA500-\uA60C\uA610-\uA62B\uA640-\uA65F\uA662-\uA66F\uA67C\uA67D\uA67F-\uA697\uA6A0-\uA6F1\uA717-\uA71F\uA722-\uA788\uA78B\uA78C\uA7FB-\uA827\uA840-\uA873\uA880-\uA8C4\uA8D0-\uA8D9\uA8E0-\uA8F7\uA8FB\uA900-\uA92D\uA930-\uA953\uA960-\uA97C\uA980-\uA9C0\uA9CF-\uA9D9\uAA00-\uAA36\uAA40-\uAA4D\uAA50-\uAA59\uAA60-\uAA76\uAA7A\uAA7B\uAA80-\uAAC2\uAADB-\uAADD\uABC0-\uABEA\uABEC\uABED\uABF0-\uABF9\uAC00-\uD7A3\uD7B0-\uD7C6\uD7CB-\uD7FB\uF900-\uFA2D\uFA30-\uFA6D\uFA70-\uFAD9\uFB00-\uFB06\uFB13-\uFB17\uFB1D-\uFB28\uFB2A-\uFB36\uFB38-\uFB3C\uFB3E\uFB40\uFB41\uFB43\uFB44\uFB46-\uFBB1\uFBD3-\uFD3D\uFD50-\uFD8F\uFD92-\uFDC7\uFDF0-\uFDFB\uFE00-\uFE0F\uFE20-\uFE26\uFE33\uFE34\uFE4D-\uFE4F\uFE70-\uFE74\uFE76-\uFEFC\uFF10-\uFF19\uFF21-\uFF3A\uFF3F\uFF41-\uFF5A\uFF66-\uFFBE\uFFC2-\uFFC7\uFFCA-\uFFCF\uFFD2-\uFFD7\uFFDA-\uFFDC])*$/; },{}],3:[function(require,module,exports){ var str = '183,768,769,770,771,772,773,774,775,776,777,778,779,780,781,782,783,784,785,786,787,788,789,790,791,792,793,794,795,796,797,798,799,800,801,802,803,804,805,806,807,808,809,810,811,812,813,814,815,816,817,818,819,820,821,822,823,824,825,826,827,828,829,830,831,832,833,834,835,836,837,838,839,840,841,842,843,844,845,846,847,848,849,850,851,852,853,854,855,856,857,858,859,860,861,862,863,864,865,866,867,868,869,870,871,872,873,874,875,876,877,878,879,903,1155,1156,1157,1158,1159,1425,1426,1427,1428,1429,1430,1431,1432,1433,1434,1435,1436,1437,1438,1439,1440,1441,1442,1443,1444,1445,1446,1447,1448,1449,1450,1451,1452,1453,1454,1455,1456,1457,1458,1459,1460,1461,1462,1463,1464,1465,1466,1467,1468,1469,1471,1473,1474,1476,1477,1479,1552,1553,1554,1555,1556,1557,1558,1559,1560,1561,1562,1611,1612,1613,1614,1615,1616,1617,1618,1619,1620,1621,1622,1623,1624,1625,1626,1627,1628,1629,1630,1631,1632,1633,1634,1635,1636,1637,1638,1639,1640,1641,1648,1750,1751,1752,1753,1754,1755,1756,1759,1760,1761,1762,1763,1764,1767,1768,1770,1771,1772,1773,1776,1777,1778,1779,1780,1781,1782,1783,1784,1785,1809,1840,1841,1842,1843,1844,1845,1846,1847,1848,1849,1850,1851,1852,1853,1854,1855,1856,1857,1858,1859,1860,1861,1862,1863,1864,1865,1866,1958,1959,1960,1961,1962,1963,1964,1965,1966,1967,1968,1984,1985,1986,1987,1988,1989,1990,1991,1992,1993,2027,2028,2029,2030,2031,2032,2033,2034,2035,2045,2070,2071,2072,2073,2075,2076,2077,2078,2079,2080,2081,2082,2083,2085,2086,2087,2089,2090,2091,2092,2093,2137,2138,2139,2259,2260,2261,2262,2263,2264,2265,2266,2267,2268,2269,2270,2271,2272,2273,2275,2276,2277,2278,2279,2280,2281,2282,2283,2284,2285,2286,2287,2288,2289,2290,2291,2292,2293,2294,2295,2296,2297,2298,2299,2300,2301,2302,2303,2304,2305,2306,2307,2362,2363,2364,2366,2367,2368,2369,2370,2371,2372,2373,2374,2375,2376,2377,2378,2379,2380,2381,2382,2383,2385,2386,2387,2388,2389,2390,2391,2402,2403,2406,2407,2408,2409,2410,2411,2412,2413,2414,2415,2433,2434,2435,2492,2494,2495,2496,2497,2498,2499,2500,2503,2504,2507,2508,2509,2519,2530,2531,2534,2535,2536,2537,2538,2539,2540,2541,2542,2543,2558,2561,2562,2563,2620,2622,2623,2624,2625,2626,2631,2632,2635,2636,2637,2641,2662,2663,2664,2665,2666,2667,2668,2669,2670,2671,2672,2673,2677,2689,2690,2691,2748,2750,2751,2752,2753,2754,2755,2756,2757,2759,2760,2761,2763,2764,2765,2786,2787,2790,2791,2792,2793,2794,2795,2796,2797,2798,2799,2810,2811,2812,2813,2814,2815,2817,2818,2819,2876,2878,2879,2880,2881,2882,2883,2884,2887,2888,2891,2892,2893,2902,2903,2914,2915,2918,2919,2920,2921,2922,2923,2924,2925,2926,2927,2946,3006,3007,3008,3009,3010,3014,3015,3016,3018,3019,3020,3021,3031,3046,3047,3048,3049,3050,3051,3052,3053,3054,3055,3072,3073,3074,3075,3076,3134,3135,3136,3137,3138,3139,3140,3142,3143,3144,3146,3147,3148,3149,3157,3158,3170,3171,3174,3175,3176,3177,3178,3179,3180,3181,3182,3183,3201,3202,3203,3260,3262,3263,3264,3265,3266,3267,3268,3270,3271,3272,3274,3275,3276,3277,3285,3286,3298,3299,3302,3303,3304,3305,3306,3307,3308,3309,3310,3311,3328,3329,3330,3331,3387,3388,3390,3391,3392,3393,3394,3395,3396,3398,3399,3400,3402,3403,3404,3405,3415,3426,3427,3430,3431,3432,3433,3434,3435,3436,3437,3438,3439,3458,3459,3530,3535,3536,3537,3538,3539,3540,3542,3544,3545,3546,3547,3548,3549,3550,3551,3558,3559,3560,3561,3562,3563,3564,3565,3566,3567,3570,3571,3633,3636,3637,3638,3639,3640,3641,3642,3655,3656,3657,3658,3659,3660,3661,3662,3664,3665,3666,3667,3668,3669,3670,3671,3672,3673,3761,3764,3765,3766,3767,3768,3769,3771,3772,3784,3785,3786,3787,3788,3789,3792,3793,3794,3795,3796,3797,3798,3799,3800,3801,3864,3865,3872,3873,3874,3875,3876,3877,3878,3879,3880,3881,3893,3895,3897,3902,3903,3953,3954,3955,3956,3957,3958,3959,3960,3961,3962,3963,3964,3965,3966,3967,3968,3969,3970,3971,3972,3974,3975,3981,3982,3983,3984,3985,3986,3987,3988,3989,3990,3991,3993,3994,3995,3996,3997,3998,3999,4000,4001,4002,4003,4004,4005,4006,4007,4008,4009,4010,4011,4012,4013,4014,4015,4016,4017,4018,4019,4020,4021,4022,4023,4024,4025,4026,4027,4028,4038,4139,4140,4141,4142,4143,4144,4145,4146,4147,4148,4149,4150,4151,4152,4153,4154,4155,4156,4157,4158,4160,4161,4162,4163,4164,4165,4166,4167,4168,4169,4182,4183,4184,4185,4190,4191,4192,4194,4195,4196,4199,4200,4201,4202,4203,4204,4205,4209,4210,4211,4212,4226,4227,4228,4229,4230,4231,4232,4233,4234,4235,4236,4237,4239,4240,4241,4242,4243,4244,4245,4246,4247,4248,4249,4250,4251,4252,4253,4957,4958,4959,4969,4970,4971,4972,4973,4974,4975,4976,4977,5906,5907,5908,5938,5939,5940,5970,5971,6002,6003,6068,6069,6070,6071,6072,6073,6074,6075,6076,6077,6078,6079,6080,6081,6082,6083,6084,6085,6086,6087,6088,6089,6090,6091,6092,6093,6094,6095,6096,6097,6098,6099,6109,6112,6113,6114,6115,6116,6117,6118,6119,6120,6121,6155,6156,6157,6160,6161,6162,6163,6164,6165,6166,6167,6168,6169,6313,6432,6433,6434,6435,6436,6437,6438,6439,6440,6441,6442,6443,6448,6449,6450,6451,6452,6453,6454,6455,6456,6457,6458,6459,6470,6471,6472,6473,6474,6475,6476,6477,6478,6479,6608,6609,6610,6611,6612,6613,6614,6615,6616,6617,6618,6679,6680,6681,6682,6683,6741,6742,6743,6744,6745,6746,6747,6748,6749,6750,6752,6753,6754,6755,6756,6757,6758,6759,6760,6761,6762,6763,6764,6765,6766,6767,6768,6769,6770,6771,6772,6773,6774,6775,6776,6777,6778,6779,6780,6783,6784,6785,6786,6787,6788,6789,6790,6791,6792,6793,6800,6801,6802,6803,6804,6805,6806,6807,6808,6809,6832,6833,6834,6835,6836,6837,6838,6839,6840,6841,6842,6843,6844,6845,6912,6913,6914,6915,6916,6964,6965,6966,6967,6968,6969,6970,6971,6972,6973,6974,6975,6976,6977,6978,6979,6980,6992,6993,6994,6995,6996,6997,6998,6999,7000,7001,7019,7020,7021,7022,7023,7024,7025,7026,7027,7040,7041,7042,7073,7074,7075,7076,7077,7078,7079,7080,7081,7082,7083,7084,7085,7088,7089,7090,7091,7092,7093,7094,7095,7096,7097,7142,7143,7144,7145,7146,7147,7148,7149,7150,7151,7152,7153,7154,7155,7204,7205,7206,7207,7208,7209,7210,7211,7212,7213,7214,7215,7216,7217,7218,7219,7220,7221,7222,7223,7232,7233,7234,7235,7236,7237,7238,7239,7240,7241,7248,7249,7250,7251,7252,7253,7254,7255,7256,7257,7376,7377,7378,7380,7381,7382,7383,7384,7385,7386,7387,7388,7389,7390,7391,7392,7393,7394,7395,7396,7397,7398,7399,7400,7405,7410,7411,7412,7415,7416,7417,7616,7617,7618,7619,7620,7621,7622,7623,7624,7625,7626,7627,7628,7629,7630,7631,7632,7633,7634,7635,7636,7637,7638,7639,7640,7641,7642,7643,7644,7645,7646,7647,7648,7649,7650,7651,7652,7653,7654,7655,7656,7657,7658,7659,7660,7661,7662,7663,7664,7665,7666,7667,7668,7669,7670,7671,7672,7673,7675,7676,7677,7678,7679,8204,8205,8255,8256,8276,8400,8401,8402,8403,8404,8405,8406,8407,8408,8409,8410,8411,8412,8417,8421,8422,8423,8424,8425,8426,8427,8428,8429,8430,8431,8432,11503,11504,11505,11647,11744,11745,11746,11747,11748,11749,11750,11751,11752,11753,11754,11755,11756,11757,11758,11759,11760,11761,11762,11763,11764,11765,11766,11767,11768,11769,11770,11771,11772,11773,11774,11775,12330,12331,12332,12333,12334,12335,12441,12442,42528,42529,42530,42531,42532,42533,42534,42535,42536,42537,42607,42612,42613,42614,42615,42616,42617,42618,42619,42620,42621,42654,42655,42736,42737,43010,43014,43019,43043,43044,43045,43046,43047,43136,43137,43188,43189,43190,43191,43192,43193,43194,43195,43196,43197,43198,43199,43200,43201,43202,43203,43204,43205,43216,43217,43218,43219,43220,43221,43222,43223,43224,43225,43232,43233,43234,43235,43236,43237,43238,43239,43240,43241,43242,43243,43244,43245,43246,43247,43248,43249,43263,43264,43265,43266,43267,43268,43269,43270,43271,43272,43273,43302,43303,43304,43305,43306,43307,43308,43309,43335,43336,43337,43338,43339,43340,43341,43342,43343,43344,43345,43346,43347,43392,43393,43394,43395,43443,43444,43445,43446,43447,43448,43449,43450,43451,43452,43453,43454,43455,43456,43472,43473,43474,43475,43476,43477,43478,43479,43480,43481,43493,43504,43505,43506,43507,43508,43509,43510,43511,43512,43513,43561,43562,43563,43564,43565,43566,43567,43568,43569,43570,43571,43572,43573,43574,43587,43596,43597,43600,43601,43602,43603,43604,43605,43606,43607,43608,43609,43643,43644,43645,43696,43698,43699,43700,43703,43704,43710,43711,43713,43755,43756,43757,43758,43759,43765,43766,44003,44004,44005,44006,44007,44008,44009,44010,44012,44013,44016,44017,44018,44019,44020,44021,44022,44023,44024,44025,64286,65024,65025,65026,65027,65028,65029,65030,65031,65032,65033,65034,65035,65036,65037,65038,65039,65056,65057,65058,65059,65060,65061,65062,65063,65064,65065,65066,65067,65068,65069,65070,65071,65075,65076,65101,65102,65103,65296,65297,65298,65299,65300,65301,65302,65303,65304,65305,65343'; var arr = str.split(',').map(function(code) { return parseInt(code, 10); }); module.exports = arr; },{}],4:[function(require,module,exports){ var str = '170,181,186,192,193,194,195,196,197,198,199,200,201,202,203,204,205,206,207,208,209,210,211,212,213,214,216,217,218,219,220,221,222,223,224,225,226,227,228,229,230,231,232,233,234,235,236,237,238,239,240,241,242,243,244,245,246,248,249,250,251,252,253,254,255,256,257,258,259,260,261,262,263,264,265,266,267,268,269,270,271,272,273,274,275,276,277,278,279,280,281,282,283,284,285,286,287,288,289,290,291,292,293,294,295,296,297,298,299,300,301,302,303,304,305,306,307,308,309,310,311,312,313,314,315,316,317,318,319,320,321,322,323,324,325,326,327,328,329,330,331,332,333,334,335,336,337,338,339,340,341,342,343,344,345,346,347,348,349,350,351,352,353,354,355,356,357,358,359,360,361,362,363,364,365,366,367,368,369,370,371,372,373,374,375,376,377,378,379,380,381,382,383,384,385,386,387,388,389,390,391,392,393,394,395,396,397,398,399,400,401,402,403,404,405,406,407,408,409,410,411,412,413,414,415,416,417,418,419,420,421,422,423,424,425,426,427,428,429,430,431,432,433,434,435,436,437,438,439,440,441,442,443,444,445,446,447,448,449,450,451,452,453,454,455,456,457,458,459,460,461,462,463,464,465,466,467,468,469,470,471,472,473,474,475,476,477,478,479,480,481,482,483,484,485,486,487,488,489,490,491,492,493,494,495,496,497,498,499,500,501,502,503,504,505,506,507,508,509,510,511,512,513,514,515,516,517,518,519,520,521,522,523,524,525,526,527,528,529,530,531,532,533,534,535,536,537,538,539,540,541,542,543,544,545,546,547,548,549,550,551,552,553,554,555,556,557,558,559,560,561,562,563,564,565,566,567,568,569,570,571,572,573,574,575,576,577,578,579,580,581,582,583,584,585,586,587,588,589,590,591,592,593,594,595,596,597,598,599,600,601,602,603,604,605,606,607,608,609,610,611,612,613,614,615,616,617,618,619,620,621,622,623,624,625,626,627,628,629,630,631,632,633,634,635,636,637,638,639,640,641,642,643,644,645,646,647,648,649,650,651,652,653,654,655,656,657,658,659,660,661,662,663,664,665,666,667,668,669,670,671,672,673,674,675,676,677,678,679,680,681,682,683,684,685,686,687,688,689,690,691,692,693,694,695,696,697,698,699,700,701,702,703,704,705,710,711,712,713,714,715,716,717,718,719,720,721,736,737,738,739,740,748,750,880,881,882,883,884,886,887,890,891,892,893,895,902,904,905,906,908,910,911,912,913,914,915,916,917,918,919,920,921,922,923,924,925,926,927,928,929,931,932,933,934,935,936,937,938,939,940,941,942,943,944,945,946,947,948,949,950,951,952,953,954,955,956,957,958,959,960,961,962,963,964,965,966,967,968,969,970,971,972,973,974,975,976,977,978,979,980,981,982,983,984,985,986,987,988,989,990,991,992,993,994,995,996,997,998,999,1000,1001,1002,1003,1004,1005,1006,1007,1008,1009,1010,1011,1012,1013,1015,1016,1017,1018,1019,1020,1021,1022,1023,1024,1025,1026,1027,1028,1029,1030,1031,1032,1033,1034,1035,1036,1037,1038,1039,1040,1041,1042,1043,1044,1045,1046,1047,1048,1049,1050,1051,1052,1053,1054,1055,1056,1057,1058,1059,1060,1061,1062,1063,1064,1065,1066,1067,1068,1069,1070,1071,1072,1073,1074,1075,1076,1077,1078,1079,1080,1081,1082,1083,1084,1085,1086,1087,1088,1089,1090,1091,1092,1093,1094,1095,1096,1097,1098,1099,1100,1101,1102,1103,1104,1105,1106,1107,1108,1109,1110,1111,1112,1113,1114,1115,1116,1117,1118,1119,1120,1121,1122,1123,1124,1125,1126,1127,1128,1129,1130,1131,1132,1133,1134,1135,1136,1137,1138,1139,1140,1141,1142,1143,1144,1145,1146,1147,1148,1149,1150,1151,1152,1153,1162,1163,1164,1165,1166,1167,1168,1169,1170,1171,1172,1173,1174,1175,1176,1177,1178,1179,1180,1181,1182,1183,1184,1185,1186,1187,1188,1189,1190,1191,1192,1193,1194,1195,1196,1197,1198,1199,1200,1201,1202,1203,1204,1205,1206,1207,1208,1209,1210,1211,1212,1213,1214,1215,1216,1217,1218,1219,1220,1221,1222,1223,1224,1225,1226,1227,1228,1229,1230,1231,1232,1233,1234,1235,1236,1237,1238,1239,1240,1241,1242,1243,1244,1245,1246,1247,1248,1249,1250,1251,1252,1253,1254,1255,1256,1257,1258,1259,1260,1261,1262,1263,1264,1265,1266,1267,1268,1269,1270,1271,1272,1273,1274,1275,1276,1277,1278,1279,1280,1281,1282,1283,1284,1285,1286,1287,1288,1289,1290,1291,1292,1293,1294,1295,1296,1297,1298,1299,1300,1301,1302,1303,1304,1305,1306,1307,1308,1309,1310,1311,1312,1313,1314,1315,1316,1317,1318,1319,1320,1321,1322,1323,1324,1325,1326,1327,1329,1330,1331,1332,1333,1334,1335,1336,1337,1338,1339,1340,1341,1342,1343,1344,1345,1346,1347,1348,1349,1350,1351,1352,1353,1354,1355,1356,1357,1358,1359,1360,1361,1362,1363,1364,1365,1366,1369,1376,1377,1378,1379,1380,1381,1382,1383,1384,1385,1386,1387,1388,1389,1390,1391,1392,1393,1394,1395,1396,1397,1398,1399,1400,1401,1402,1403,1404,1405,1406,1407,1408,1409,1410,1411,1412,1413,1414,1415,1416,1488,1489,1490,1491,1492,1493,1494,1495,1496,1497,1498,1499,1500,1501,1502,1503,1504,1505,1506,1507,1508,1509,1510,1511,1512,1513,1514,1519,1520,1521,1522,1568,1569,1570,1571,1572,1573,1574,1575,1576,1577,1578,1579,1580,1581,1582,1583,1584,1585,1586,1587,1588,1589,1590,1591,1592,1593,1594,1595,1596,1597,1598,1599,1600,1601,1602,1603,1604,1605,1606,1607,1608,1609,1610,1646,1647,1649,1650,1651,1652,1653,1654,1655,1656,1657,1658,1659,1660,1661,1662,1663,1664,1665,1666,1667,1668,1669,1670,1671,1672,1673,1674,1675,1676,1677,1678,1679,1680,1681,1682,1683,1684,1685,1686,1687,1688,1689,1690,1691,1692,1693,1694,1695,1696,1697,1698,1699,1700,1701,1702,1703,1704,1705,1706,1707,1708,1709,1710,1711,1712,1713,1714,1715,1716,1717,1718,1719,1720,1721,1722,1723,1724,1725,1726,1727,1728,1729,1730,1731,1732,1733,1734,1735,1736,1737,1738,1739,1740,1741,1742,1743,1744,1745,1746,1747,1749,1765,1766,1774,1775,1786,1787,1788,1791,1808,1810,1811,1812,1813,1814,1815,1816,1817,1818,1819,1820,1821,1822,1823,1824,1825,1826,1827,1828,1829,1830,1831,1832,1833,1834,1835,1836,1837,1838,1839,1869,1870,1871,1872,1873,1874,1875,1876,1877,1878,1879,1880,1881,1882,1883,1884,1885,1886,1887,1888,1889,1890,1891,1892,1893,1894,1895,1896,1897,1898,1899,1900,1901,1902,1903,1904,1905,1906,1907,1908,1909,1910,1911,1912,1913,1914,1915,1916,1917,1918,1919,1920,1921,1922,1923,1924,1925,1926,1927,1928,1929,1930,1931,1932,1933,1934,1935,1936,1937,1938,1939,1940,1941,1942,1943,1944,1945,1946,1947,1948,1949,1950,1951,1952,1953,1954,1955,1956,1957,1969,1994,1995,1996,1997,1998,1999,2000,2001,2002,2003,2004,2005,2006,2007,2008,2009,2010,2011,2012,2013,2014,2015,2016,2017,2018,2019,2020,2021,2022,2023,2024,2025,2026,2036,2037,2042,2048,2049,2050,2051,2052,2053,2054,2055,2056,2057,2058,2059,2060,2061,2062,2063,2064,2065,2066,2067,2068,2069,2074,2084,2088,2112,2113,2114,2115,2116,2117,2118,2119,2120,2121,2122,2123,2124,2125,2126,2127,2128,2129,2130,2131,2132,2133,2134,2135,2136,2144,2145,2146,2147,2148,2149,2150,2151,2152,2153,2154,2208,2209,2210,2211,2212,2213,2214,2215,2216,2217,2218,2219,2220,2221,2222,2223,2224,2225,2226,2227,2228,2230,2231,2232,2233,2234,2235,2236,2237,2308,2309,2310,2311,2312,2313,2314,2315,2316,2317,2318,2319,2320,2321,2322,2323,2324,2325,2326,2327,2328,2329,2330,2331,2332,2333,2334,2335,2336,2337,2338,2339,2340,2341,2342,2343,2344,2345,2346,2347,2348,2349,2350,2351,2352,2353,2354,2355,2356,2357,2358,2359,2360,2361,2365,2384,2392,2393,2394,2395,2396,2397,2398,2399,2400,2401,2417,2418,2419,2420,2421,2422,2423,2424,2425,2426,2427,2428,2429,2430,2431,2432,2437,2438,2439,2440,2441,2442,2443,2444,2447,2448,2451,2452,2453,2454,2455,2456,2457,2458,2459,2460,2461,2462,2463,2464,2465,2466,2467,2468,2469,2470,2471,2472,2474,2475,2476,2477,2478,2479,2480,2482,2486,2487,2488,2489,2493,2510,2524,2525,2527,2528,2529,2544,2545,2556,2565,2566,2567,2568,2569,2570,2575,2576,2579,2580,2581,2582,2583,2584,2585,2586,2587,2588,2589,2590,2591,2592,2593,2594,2595,2596,2597,2598,2599,2600,2602,2603,2604,2605,2606,2607,2608,2610,2611,2613,2614,2616,2617,2649,2650,2651,2652,2654,2674,2675,2676,2693,2694,2695,2696,2697,2698,2699,2700,2701,2703,2704,2705,2707,2708,2709,2710,2711,2712,2713,2714,2715,2716,2717,2718,2719,2720,2721,2722,2723,2724,2725,2726,2727,2728,2730,2731,2732,2733,2734,2735,2736,2738,2739,2741,2742,2743,2744,2745,2749,2768,2784,2785,2809,2821,2822,2823,2824,2825,2826,2827,2828,2831,2832,2835,2836,2837,2838,2839,2840,2841,2842,2843,2844,2845,2846,2847,2848,2849,2850,2851,2852,2853,2854,2855,2856,2858,2859,2860,2861,2862,2863,2864,2866,2867,2869,2870,2871,2872,2873,2877,2908,2909,2911,2912,2913,2929,2947,2949,2950,2951,2952,2953,2954,2958,2959,2960,2962,2963,2964,2965,2969,2970,2972,2974,2975,2979,2980,2984,2985,2986,2990,2991,2992,2993,2994,2995,2996,2997,2998,2999,3000,3001,3024,3077,3078,3079,3080,3081,3082,3083,3084,3086,3087,3088,3090,3091,3092,3093,3094,3095,3096,3097,3098,3099,3100,3101,3102,3103,3104,3105,3106,3107,3108,3109,3110,3111,3112,3114,3115,3116,3117,3118,3119,3120,3121,3122,3123,3124,3125,3126,3127,3128,3129,3133,3160,3161,3162,3168,3169,3200,3205,3206,3207,3208,3209,3210,3211,3212,3214,3215,3216,3218,3219,3220,3221,3222,3223,3224,3225,3226,3227,3228,3229,3230,3231,3232,3233,3234,3235,3236,3237,3238,3239,3240,3242,3243,3244,3245,3246,3247,3248,3249,3250,3251,3253,3254,3255,3256,3257,3261,3294,3296,3297,3313,3314,3333,3334,3335,3336,3337,3338,3339,3340,3342,3343,3344,3346,3347,3348,3349,3350,3351,3352,3353,3354,3355,3356,3357,3358,3359,3360,3361,3362,3363,3364,3365,3366,3367,3368,3369,3370,3371,3372,3373,3374,3375,3376,3377,3378,3379,3380,3381,3382,3383,3384,3385,3386,3389,3406,3412,3413,3414,3423,3424,3425,3450,3451,3452,3453,3454,3455,3461,3462,3463,3464,3465,3466,3467,3468,3469,3470,3471,3472,3473,3474,3475,3476,3477,3478,3482,3483,3484,3485,3486,3487,3488,3489,3490,3491,3492,3493,3494,3495,3496,3497,3498,3499,3500,3501,3502,3503,3504,3505,3507,3508,3509,3510,3511,3512,3513,3514,3515,3517,3520,3521,3522,3523,3524,3525,3526,3585,3586,3587,3588,3589,3590,3591,3592,3593,3594,3595,3596,3597,3598,3599,3600,3601,3602,3603,3604,3605,3606,3607,3608,3609,3610,3611,3612,3613,3614,3615,3616,3617,3618,3619,3620,3621,3622,3623,3624,3625,3626,3627,3628,3629,3630,3631,3632,3634,3635,3648,3649,3650,3651,3652,3653,3654,3713,3714,3716,3719,3720,3722,3725,3732,3733,3734,3735,3737,3738,3739,3740,3741,3742,3743,3745,3746,3747,3749,3751,3754,3755,3757,3758,3759,3760,3762,3763,3773,3776,3777,3778,3779,3780,3782,3804,3805,3806,3807,3840,3904,3905,3906,3907,3908,3909,3910,3911,3913,3914,3915,3916,3917,3918,3919,3920,3921,3922,3923,3924,3925,3926,3927,3928,3929,3930,3931,3932,3933,3934,3935,3936,3937,3938,3939,3940,3941,3942,3943,3944,3945,3946,3947,3948,3976,3977,3978,3979,3980,4096,4097,4098,4099,4100,4101,4102,4103,4104,4105,4106,4107,4108,4109,4110,4111,4112,4113,4114,4115,4116,4117,4118,4119,4120,4121,4122,4123,4124,4125,4126,4127,4128,4129,4130,4131,4132,4133,4134,4135,4136,4137,4138,4159,4176,4177,4178,4179,4180,4181,4186,4187,4188,4189,4193,4197,4198,4206,4207,4208,4213,4214,4215,4216,4217,4218,4219,4220,4221,4222,4223,4224,4225,4238,4256,4257,4258,4259,4260,4261,4262,4263,4264,4265,4266,4267,4268,4269,4270,4271,4272,4273,4274,4275,4276,4277,4278,4279,4280,4281,4282,4283,4284,4285,4286,4287,4288,4289,4290,4291,4292,4293,4295,4301,4304,4305,4306,4307,4308,4309,4310,4311,4312,4313,4314,4315,4316,4317,4318,4319,4320,4321,4322,4323,4324,4325,4326,4327,4328,4329,4330,4331,4332,4333,4334,4335,4336,4337,4338,4339,4340,4341,4342,4343,4344,4345,4346,4348,4349,4350,4351,4352,4353,4354,4355,4356,4357,4358,4359,4360,4361,4362,4363,4364,4365,4366,4367,4368,4369,4370,4371,4372,4373,4374,4375,4376,4377,4378,4379,4380,4381,4382,4383,4384,4385,4386,4387,4388,4389,4390,4391,4392,4393,4394,4395,4396,4397,4398,4399,4400,4401,4402,4403,4404,4405,4406,4407,4408,4409,4410,4411,4412,4413,4414,4415,4416,4417,4418,4419,4420,4421,4422,4423,4424,4425,4426,4427,4428,4429,4430,4431,4432,4433,4434,4435,4436,4437,4438,4439,4440,4441,4442,4443,4444,4445,4446,4447,4448,4449,4450,4451,4452,4453,4454,4455,4456,4457,4458,4459,4460,4461,4462,4463,4464,4465,4466,4467,4468,4469,4470,4471,4472,4473,4474,4475,4476,4477,4478,4479,4480,4481,4482,4483,4484,4485,4486,4487,4488,4489,4490,4491,4492,4493,4494,4495,4496,4497,4498,4499,4500,4501,4502,4503,4504,4505,4506,4507,4508,4509,4510,4511,4512,4513,4514,4515,4516,4517,4518,4519,4520,4521,4522,4523,4524,4525,4526,4527,4528,4529,4530,4531,4532,4533,4534,4535,4536,4537,4538,4539,4540,4541,4542,4543,4544,4545,4546,4547,4548,4549,4550,4551,4552,4553,4554,4555,4556,4557,4558,4559,4560,4561,4562,4563,4564,4565,4566,4567,4568,4569,4570,4571,4572,4573,4574,4575,4576,4577,4578,4579,4580,4581,4582,4583,4584,4585,4586,4587,4588,4589,4590,4591,4592,4593,4594,4595,4596,4597,4598,4599,4600,4601,4602,4603,4604,4605,4606,4607,4608,4609,4610,4611,4612,4613,4614,4615,4616,4617,4618,4619,4620,4621,4622,4623,4624,4625,4626,4627,4628,4629,4630,4631,4632,4633,4634,4635,4636,4637,4638,4639,4640,4641,4642,4643,4644,4645,4646,4647,4648,4649,4650,4651,4652,4653,4654,4655,4656,4657,4658,4659,4660,4661,4662,4663,4664,4665,4666,4667,4668,4669,4670,4671,4672,4673,4674,4675,4676,4677,4678,4679,4680,4682,4683,4684,4685,4688,4689,4690,4691,4692,4693,4694,4696,4698,4699,4700,4701,4704,4705,4706,4707,4708,4709,4710,4711,4712,4713,4714,4715,4716,4717,4718,4719,4720,4721,4722,4723,4724,4725,4726,4727,4728,4729,4730,4731,4732,4733,4734,4735,4736,4737,4738,4739,4740,4741,4742,4743,4744,4746,4747,4748,4749,4752,4753,4754,4755,4756,4757,4758,4759,4760,4761,4762,4763,4764,4765,4766,4767,4768,4769,4770,4771,4772,4773,4774,4775,4776,4777,4778,4779,4780,4781,4782,4783,4784,4786,4787,4788,4789,4792,4793,4794,4795,4796,4797,4798,4800,4802,4803,4804,4805,4808,4809,4810,4811,4812,4813,4814,4815,4816,4817,4818,4819,4820,4821,4822,4824,4825,4826,4827,4828,4829,4830,4831,4832,4833,4834,4835,4836,4837,4838,4839,4840,4841,4842,4843,4844,4845,4846,4847,4848,4849,4850,4851,4852,4853,4854,4855,4856,4857,4858,4859,4860,4861,4862,4863,4864,4865,4866,4867,4868,4869,4870,4871,4872,4873,4874,4875,4876,4877,4878,4879,4880,4882,4883,4884,4885,4888,4889,4890,4891,4892,4893,4894,4895,4896,4897,4898,4899,4900,4901,4902,4903,4904,4905,4906,4907,4908,4909,4910,4911,4912,4913,4914,4915,4916,4917,4918,4919,4920,4921,4922,4923,4924,4925,4926,4927,4928,4929,4930,4931,4932,4933,4934,4935,4936,4937,4938,4939,4940,4941,4942,4943,4944,4945,4946,4947,4948,4949,4950,4951,4952,4953,4954,4992,4993,4994,4995,4996,4997,4998,4999,5000,5001,5002,5003,5004,5005,5006,5007,5024,5025,5026,5027,5028,5029,5030,5031,5032,5033,5034,5035,5036,5037,5038,5039,5040,5041,5042,5043,5044,5045,5046,5047,5048,5049,5050,5051,5052,5053,5054,5055,5056,5057,5058,5059,5060,5061,5062,5063,5064,5065,5066,5067,5068,5069,5070,5071,5072,5073,5074,5075,5076,5077,5078,5079,5080,5081,5082,5083,5084,5085,5086,5087,5088,5089,5090,5091,5092,5093,5094,5095,5096,5097,5098,5099,5100,5101,5102,5103,5104,5105,5106,5107,5108,5109,5112,5113,5114,5115,5116,5117,5121,5122,5123,5124,5125,5126,5127,5128,5129,5130,5131,5132,5133,5134,5135,5136,5137,5138,5139,5140,5141,5142,5143,5144,5145,5146,5147,5148,5149,5150,5151,5152,5153,5154,5155,5156,5157,5158,5159,5160,5161,5162,5163,5164,5165,5166,5167,5168,5169,5170,5171,5172,5173,5174,5175,5176,5177,5178,5179,5180,5181,5182,5183,5184,5185,5186,5187,5188,5189,5190,5191,5192,5193,5194,5195,5196,5197,5198,5199,5200,5201,5202,5203,5204,5205,5206,5207,5208,5209,5210,5211,5212,5213,5214,5215,5216,5217,5218,5219,5220,5221,5222,5223,5224,5225,5226,5227,5228,5229,5230,5231,5232,5233,5234,5235,5236,5237,5238,5239,5240,5241,5242,5243,5244,5245,5246,5247,5248,5249,5250,5251,5252,5253,5254,5255,5256,5257,5258,5259,5260,5261,5262,5263,5264,5265,5266,5267,5268,5269,5270,5271,5272,5273,5274,5275,5276,5277,5278,5279,5280,5281,5282,5283,5284,5285,5286,5287,5288,5289,5290,5291,5292,5293,5294,5295,5296,5297,5298,5299,5300,5301,5302,5303,5304,5305,5306,5307,5308,5309,5310,5311,5312,5313,5314,5315,5316,5317,5318,5319,5320,5321,5322,5323,5324,5325,5326,5327,5328,5329,5330,5331,5332,5333,5334,5335,5336,5337,5338,5339,5340,5341,5342,5343,5344,5345,5346,5347,5348,5349,5350,5351,5352,5353,5354,5355,5356,5357,5358,5359,5360,5361,5362,5363,5364,5365,5366,5367,5368,5369,5370,5371,5372,5373,5374,5375,5376,5377,5378,5379,5380,5381,5382,5383,5384,5385,5386,5387,5388,5389,5390,5391,5392,5393,5394,5395,5396,5397,5398,5399,5400,5401,5402,5403,5404,5405,5406,5407,5408,5409,5410,5411,5412,5413,5414,5415,5416,5417,5418,5419,5420,5421,5422,5423,5424,5425,5426,5427,5428,5429,5430,5431,5432,5433,5434,5435,5436,5437,5438,5439,5440,5441,5442,5443,5444,5445,5446,5447,5448,5449,5450,5451,5452,5453,5454,5455,5456,5457,5458,5459,5460,5461,5462,5463,5464,5465,5466,5467,5468,5469,5470,5471,5472,5473,5474,5475,5476,5477,5478,5479,5480,5481,5482,5483,5484,5485,5486,5487,5488,5489,5490,5491,5492,5493,5494,5495,5496,5497,5498,5499,5500,5501,5502,5503,5504,5505,5506,5507,5508,5509,5510,5511,5512,5513,5514,5515,5516,5517,5518,5519,5520,5521,5522,5523,5524,5525,5526,5527,5528,5529,5530,5531,5532,5533,5534,5535,5536,5537,5538,5539,5540,5541,5542,5543,5544,5545,5546,5547,5548,5549,5550,5551,5552,5553,5554,5555,5556,5557,5558,5559,5560,5561,5562,5563,5564,5565,5566,5567,5568,5569,5570,5571,5572,5573,5574,5575,5576,5577,5578,5579,5580,5581,5582,5583,5584,5585,5586,5587,5588,5589,5590,5591,5592,5593,5594,5595,5596,5597,5598,5599,5600,5601,5602,5603,5604,5605,5606,5607,5608,5609,5610,5611,5612,5613,5614,5615,5616,5617,5618,5619,5620,5621,5622,5623,5624,5625,5626,5627,5628,5629,5630,5631,5632,5633,5634,5635,5636,5637,5638,5639,5640,5641,5642,5643,5644,5645,5646,5647,5648,5649,5650,5651,5652,5653,5654,5655,5656,5657,5658,5659,5660,5661,5662,5663,5664,5665,5666,5667,5668,5669,5670,5671,5672,5673,5674,5675,5676,5677,5678,5679,5680,5681,5682,5683,5684,5685,5686,5687,5688,5689,5690,5691,5692,5693,5694,5695,5696,5697,5698,5699,5700,5701,5702,5703,5704,5705,5706,5707,5708,5709,5710,5711,5712,5713,5714,5715,5716,5717,5718,5719,5720,5721,5722,5723,5724,5725,5726,5727,5728,5729,5730,5731,5732,5733,5734,5735,5736,5737,5738,5739,5740,5743,5744,5745,5746,5747,5748,5749,5750,5751,5752,5753,5754,5755,5756,5757,5758,5759,5761,5762,5763,5764,5765,5766,5767,5768,5769,5770,5771,5772,5773,5774,5775,5776,5777,5778,5779,5780,5781,5782,5783,5784,5785,5786,5792,5793,5794,5795,5796,5797,5798,5799,5800,5801,5802,5803,5804,5805,5806,5807,5808,5809,5810,5811,5812,5813,5814,5815,5816,5817,5818,5819,5820,5821,5822,5823,5824,5825,5826,5827,5828,5829,5830,5831,5832,5833,5834,5835,5836,5837,5838,5839,5840,5841,5842,5843,5844,5845,5846,5847,5848,5849,5850,5851,5852,5853,5854,5855,5856,5857,5858,5859,5860,5861,5862,5863,5864,5865,5866,5870,5871,5872,5873,5874,5875,5876,5877,5878,5879,5880,5888,5889,5890,5891,5892,5893,5894,5895,5896,5897,5898,5899,5900,5902,5903,5904,5905,5920,5921,5922,5923,5924,5925,5926,5927,5928,5929,5930,5931,5932,5933,5934,5935,5936,5937,5952,5953,5954,5955,5956,5957,5958,5959,5960,5961,5962,5963,5964,5965,5966,5967,5968,5969,5984,5985,5986,5987,5988,5989,5990,5991,5992,5993,5994,5995,5996,5998,5999,6000,6016,6017,6018,6019,6020,6021,6022,6023,6024,6025,6026,6027,6028,6029,6030,6031,6032,6033,6034,6035,6036,6037,6038,6039,6040,6041,6042,6043,6044,6045,6046,6047,6048,6049,6050,6051,6052,6053,6054,6055,6056,6057,6058,6059,6060,6061,6062,6063,6064,6065,6066,6067,6103,6108,6176,6177,6178,6179,6180,6181,6182,6183,6184,6185,6186,6187,6188,6189,6190,6191,6192,6193,6194,6195,6196,6197,6198,6199,6200,6201,6202,6203,6204,6205,6206,6207,6208,6209,6210,6211,6212,6213,6214,6215,6216,6217,6218,6219,6220,6221,6222,6223,6224,6225,6226,6227,6228,6229,6230,6231,6232,6233,6234,6235,6236,6237,6238,6239,6240,6241,6242,6243,6244,6245,6246,6247,6248,6249,6250,6251,6252,6253,6254,6255,6256,6257,6258,6259,6260,6261,6262,6263,6264,6272,6273,6274,6275,6276,6277,6278,6279,6280,6281,6282,6283,6284,6285,6286,6287,6288,6289,6290,6291,6292,6293,6294,6295,6296,6297,6298,6299,6300,6301,6302,6303,6304,6305,6306,6307,6308,6309,6310,6311,6312,6314,6320,6321,6322,6323,6324,6325,6326,6327,6328,6329,6330,6331,6332,6333,6334,6335,6336,6337,6338,6339,6340,6341,6342,6343,6344,6345,6346,6347,6348,6349,6350,6351,6352,6353,6354,6355,6356,6357,6358,6359,6360,6361,6362,6363,6364,6365,6366,6367,6368,6369,6370,6371,6372,6373,6374,6375,6376,6377,6378,6379,6380,6381,6382,6383,6384,6385,6386,6387,6388,6389,6400,6401,6402,6403,6404,6405,6406,6407,6408,6409,6410,6411,6412,6413,6414,6415,6416,6417,6418,6419,6420,6421,6422,6423,6424,6425,6426,6427,6428,6429,6430,6480,6481,6482,6483,6484,6485,6486,6487,6488,6489,6490,6491,6492,6493,6494,6495,6496,6497,6498,6499,6500,6501,6502,6503,6504,6505,6506,6507,6508,6509,6512,6513,6514,6515,6516,6528,6529,6530,6531,6532,6533,6534,6535,6536,6537,6538,6539,6540,6541,6542,6543,6544,6545,6546,6547,6548,6549,6550,6551,6552,6553,6554,6555,6556,6557,6558,6559,6560,6561,6562,6563,6564,6565,6566,6567,6568,6569,6570,6571,6576,6577,6578,6579,6580,6581,6582,6583,6584,6585,6586,6587,6588,6589,6590,6591,6592,6593,6594,6595,6596,6597,6598,6599,6600,6601,6656,6657,6658,6659,6660,6661,6662,6663,6664,6665,6666,6667,6668,6669,6670,6671,6672,6673,6674,6675,6676,6677,6678,6688,6689,6690,6691,6692,6693,6694,6695,6696,6697,6698,6699,6700,6701,6702,6703,6704,6705,6706,6707,6708,6709,6710,6711,6712,6713,6714,6715,6716,6717,6718,6719,6720,6721,6722,6723,6724,6725,6726,6727,6728,6729,6730,6731,6732,6733,6734,6735,6736,6737,6738,6739,6740,6823,6917,6918,6919,6920,6921,6922,6923,6924,6925,6926,6927,6928,6929,6930,6931,6932,6933,6934,6935,6936,6937,6938,6939,6940,6941,6942,6943,6944,6945,6946,6947,6948,6949,6950,6951,6952,6953,6954,6955,6956,6957,6958,6959,6960,6961,6962,6963,6981,6982,6983,6984,6985,6986,6987,7043,7044,7045,7046,7047,7048,7049,7050,7051,7052,7053,7054,7055,7056,7057,7058,7059,7060,7061,7062,7063,7064,7065,7066,7067,7068,7069,7070,7071,7072,7086,7087,7098,7099,7100,7101,7102,7103,7104,7105,7106,7107,7108,7109,7110,7111,7112,7113,7114,7115,7116,7117,7118,7119,7120,7121,7122,7123,7124,7125,7126,7127,7128,7129,7130,7131,7132,7133,7134,7135,7136,7137,7138,7139,7140,7141,7168,7169,7170,7171,7172,7173,7174,7175,7176,7177,7178,7179,7180,7181,7182,7183,7184,7185,7186,7187,7188,7189,7190,7191,7192,7193,7194,7195,7196,7197,7198,7199,7200,7201,7202,7203,7245,7246,7247,7258,7259,7260,7261,7262,7263,7264,7265,7266,7267,7268,7269,7270,7271,7272,7273,7274,7275,7276,7277,7278,7279,7280,7281,7282,7283,7284,7285,7286,7287,7288,7289,7290,7291,7292,7293,7296,7297,7298,7299,7300,7301,7302,7303,7304,7312,7313,7314,7315,7316,7317,7318,7319,7320,7321,7322,7323,7324,7325,7326,7327,7328,7329,7330,7331,7332,7333,7334,7335,7336,7337,7338,7339,7340,7341,7342,7343,7344,7345,7346,7347,7348,7349,7350,7351,7352,7353,7354,7357,7358,7359,7401,7402,7403,7404,7406,7407,7408,7409,7413,7414,7424,7425,7426,7427,7428,7429,7430,7431,7432,7433,7434,7435,7436,7437,7438,7439,7440,7441,7442,7443,7444,7445,7446,7447,7448,7449,7450,7451,7452,7453,7454,7455,7456,7457,7458,7459,7460,7461,7462,7463,7464,7465,7466,7467,7468,7469,7470,7471,7472,7473,7474,7475,7476,7477,7478,7479,7480,7481,7482,7483,7484,7485,7486,7487,7488,7489,7490,7491,7492,7493,7494,7495,7496,7497,7498,7499,7500,7501,7502,7503,7504,7505,7506,7507,7508,7509,7510,7511,7512,7513,7514,7515,7516,7517,7518,7519,7520,7521,7522,7523,7524,7525,7526,7527,7528,7529,7530,7531,7532,7533,7534,7535,7536,7537,7538,7539,7540,7541,7542,7543,7544,7545,7546,7547,7548,7549,7550,7551,7552,7553,7554,7555,7556,7557,7558,7559,7560,7561,7562,7563,7564,7565,7566,7567,7568,7569,7570,7571,7572,7573,7574,7575,7576,7577,7578,7579,7580,7581,7582,7583,7584,7585,7586,7587,7588,7589,7590,7591,7592,7593,7594,7595,7596,7597,7598,7599,7600,7601,7602,7603,7604,7605,7606,7607,7608,7609,7610,7611,7612,7613,7614,7615,7680,7681,7682,7683,7684,7685,7686,7687,7688,7689,7690,7691,7692,7693,7694,7695,7696,7697,7698,7699,7700,7701,7702,7703,7704,7705,7706,7707,7708,7709,7710,7711,7712,7713,7714,7715,7716,7717,7718,7719,7720,7721,7722,7723,7724,7725,7726,7727,7728,7729,7730,7731,7732,7733,7734,7735,7736,7737,7738,7739,7740,7741,7742,7743,7744,7745,7746,7747,7748,7749,7750,7751,7752,7753,7754,7755,7756,7757,7758,7759,7760,7761,7762,7763,7764,7765,7766,7767,7768,7769,7770,7771,7772,7773,7774,7775,7776,7777,7778,7779,7780,7781,7782,7783,7784,7785,7786,7787,7788,7789,7790,7791,7792,7793,7794,7795,7796,7797,7798,7799,7800,7801,7802,7803,7804,7805,7806,7807,7808,7809,7810,7811,7812,7813,7814,7815,7816,7817,7818,7819,7820,7821,7822,7823,7824,7825,7826,7827,7828,7829,7830,7831,7832,7833,7834,7835,7836,7837,7838,7839,7840,7841,7842,7843,7844,7845,7846,7847,7848,7849,7850,7851,7852,7853,7854,7855,7856,7857,7858,7859,7860,7861,7862,7863,7864,7865,7866,7867,7868,7869,7870,7871,7872,7873,7874,7875,7876,7877,7878,7879,7880,7881,7882,7883,7884,7885,7886,7887,7888,7889,7890,7891,7892,7893,7894,7895,7896,7897,7898,7899,7900,7901,7902,7903,7904,7905,7906,7907,7908,7909,7910,7911,7912,7913,7914,7915,7916,7917,7918,7919,7920,7921,7922,7923,7924,7925,7926,7927,7928,7929,7930,7931,7932,7933,7934,7935,7936,7937,7938,7939,7940,7941,7942,7943,7944,7945,7946,7947,7948,7949,7950,7951,7952,7953,7954,7955,7956,7957,7960,7961,7962,7963,7964,7965,7968,7969,7970,7971,7972,7973,7974,7975,7976,7977,7978,7979,7980,7981,7982,7983,7984,7985,7986,7987,7988,7989,7990,7991,7992,7993,7994,7995,7996,7997,7998,7999,8000,8001,8002,8003,8004,8005,8008,8009,8010,8011,8012,8013,8016,8017,8018,8019,8020,8021,8022,8023,8025,8027,8029,8031,8032,8033,8034,8035,8036,8037,8038,8039,8040,8041,8042,8043,8044,8045,8046,8047,8048,8049,8050,8051,8052,8053,8054,8055,8056,8057,8058,8059,8060,8061,8064,8065,8066,8067,8068,8069,8070,8071,8072,8073,8074,8075,8076,8077,8078,8079,8080,8081,8082,8083,8084,8085,8086,8087,8088,8089,8090,8091,8092,8093,8094,8095,8096,8097,8098,8099,8100,8101,8102,8103,8104,8105,8106,8107,8108,8109,8110,8111,8112,8113,8114,8115,8116,8118,8119,8120,8121,8122,8123,8124,8126,8130,8131,8132,8134,8135,8136,8137,8138,8139,8140,8144,8145,8146,8147,8150,8151,8152,8153,8154,8155,8160,8161,8162,8163,8164,8165,8166,8167,8168,8169,8170,8171,8172,8178,8179,8180,8182,8183,8184,8185,8186,8187,8188,8305,8319,8336,8337,8338,8339,8340,8341,8342,8343,8344,8345,8346,8347,8348,8450,8455,8458,8459,8460,8461,8462,8463,8464,8465,8466,8467,8469,8472,8473,8474,8475,8476,8477,8484,8486,8488,8490,8491,8492,8493,8494,8495,8496,8497,8498,8499,8500,8501,8502,8503,8504,8505,8508,8509,8510,8511,8517,8518,8519,8520,8521,8526,8544,8545,8546,8547,8548,8549,8550,8551,8552,8553,8554,8555,8556,8557,8558,8559,8560,8561,8562,8563,8564,8565,8566,8567,8568,8569,8570,8571,8572,8573,8574,8575,8576,8577,8578,8579,8580,8581,8582,8583,8584,11264,11265,11266,11267,11268,11269,11270,11271,11272,11273,11274,11275,11276,11277,11278,11279,11280,11281,11282,11283,11284,11285,11286,11287,11288,11289,11290,11291,11292,11293,11294,11295,11296,11297,11298,11299,11300,11301,11302,11303,11304,11305,11306,11307,11308,11309,11310,11312,11313,11314,11315,11316,11317,11318,11319,11320,11321,11322,11323,11324,11325,11326,11327,11328,11329,11330,11331,11332,11333,11334,11335,11336,11337,11338,11339,11340,11341,11342,11343,11344,11345,11346,11347,11348,11349,11350,11351,11352,11353,11354,11355,11356,11357,11358,11360,11361,11362,11363,11364,11365,11366,11367,11368,11369,11370,11371,11372,11373,11374,11375,11376,11377,11378,11379,11380,11381,11382,11383,11384,11385,11386,11387,11388,11389,11390,11391,11392,11393,11394,11395,11396,11397,11398,11399,11400,11401,11402,11403,11404,11405,11406,11407,11408,11409,11410,11411,11412,11413,11414,11415,11416,11417,11418,11419,11420,11421,11422,11423,11424,11425,11426,11427,11428,11429,11430,11431,11432,11433,11434,11435,11436,11437,11438,11439,11440,11441,11442,11443,11444,11445,11446,11447,11448,11449,11450,11451,11452,11453,11454,11455,11456,11457,11458,11459,11460,11461,11462,11463,11464,11465,11466,11467,11468,11469,11470,11471,11472,11473,11474,11475,11476,11477,11478,11479,11480,11481,11482,11483,11484,11485,11486,11487,11488,11489,11490,11491,11492,11499,11500,11501,11502,11506,11507,11520,11521,11522,11523,11524,11525,11526,11527,11528,11529,11530,11531,11532,11533,11534,11535,11536,11537,11538,11539,11540,11541,11542,11543,11544,11545,11546,11547,11548,11549,11550,11551,11552,11553,11554,11555,11556,11557,11559,11565,11568,11569,11570,11571,11572,11573,11574,11575,11576,11577,11578,11579,11580,11581,11582,11583,11584,11585,11586,11587,11588,11589,11590,11591,11592,11593,11594,11595,11596,11597,11598,11599,11600,11601,11602,11603,11604,11605,11606,11607,11608,11609,11610,11611,11612,11613,11614,11615,11616,11617,11618,11619,11620,11621,11622,11623,11631,11648,11649,11650,11651,11652,11653,11654,11655,11656,11657,11658,11659,11660,11661,11662,11663,11664,11665,11666,11667,11668,11669,11670,11680,11681,11682,11683,11684,11685,11686,11688,11689,11690,11691,11692,11693,11694,11696,11697,11698,11699,11700,11701,11702,11704,11705,11706,11707,11708,11709,11710,11712,11713,11714,11715,11716,11717,11718,11720,11721,11722,11723,11724,11725,11726,11728,11729,11730,11731,11732,11733,11734,11736,11737,11738,11739,11740,11741,11742,12293,12294,12295,12321,12322,12323,12324,12325,12326,12327,12328,12329,12337,12338,12339,12340,12341,12344,12345,12346,12347,12348,12353,12354,12355,12356,12357,12358,12359,12360,12361,12362,12363,12364,12365,12366,12367,12368,12369,12370,12371,12372,12373,12374,12375,12376,12377,12378,12379,12380,12381,12382,12383,12384,12385,12386,12387,12388,12389,12390,12391,12392,12393,12394,12395,12396,12397,12398,12399,12400,12401,12402,12403,12404,12405,12406,12407,12408,12409,12410,12411,12412,12413,12414,12415,12416,12417,12418,12419,12420,12421,12422,12423,12424,12425,12426,12427,12428,12429,12430,12431,12432,12433,12434,12435,12436,12437,12438,12443,12444,12445,12446,12447,12449,12450,12451,12452,12453,12454,12455,12456,12457,12458,12459,12460,12461,12462,12463,12464,12465,12466,12467,12468,12469,12470,12471,12472,12473,12474,12475,12476,12477,12478,12479,12480,12481,12482,12483,12484,12485,12486,12487,12488,12489,12490,12491,12492,12493,12494,12495,12496,12497,12498,12499,12500,12501,12502,12503,12504,12505,12506,12507,12508,12509,12510,12511,12512,12513,12514,12515,12516,12517,12518,12519,12520,12521,12522,12523,12524,12525,12526,12527,12528,12529,12530,12531,12532,12533,12534,12535,12536,12537,12538,12540,12541,12542,12543,12549,12550,12551,12552,12553,12554,12555,12556,12557,12558,12559,12560,12561,12562,12563,12564,12565,12566,12567,12568,12569,12570,12571,12572,12573,12574,12575,12576,12577,12578,12579,12580,12581,12582,12583,12584,12585,12586,12587,12588,12589,12590,12591,12593,12594,12595,12596,12597,12598,12599,12600,12601,12602,12603,12604,12605,12606,12607,12608,12609,12610,12611,12612,12613,12614,12615,12616,12617,12618,12619,12620,12621,12622,12623,12624,12625,12626,12627,12628,12629,12630,12631,12632,12633,12634,12635,12636,12637,12638,12639,12640,12641,12642,12643,12644,12645,12646,12647,12648,12649,12650,12651,12652,12653,12654,12655,12656,12657,12658,12659,12660,12661,12662,12663,12664,12665,12666,12667,12668,12669,12670,12671,12672,12673,12674,12675,12676,12677,12678,12679,12680,12681,12682,12683,12684,12685,12686,12704,12705,12706,12707,12708,12709,12710,12711,12712,12713,12714,12715,12716,12717,12718,12719,12720,12721,12722,12723,12724,12725,12726,12727,12728,12729,12730,12784,12785,12786,12787,12788,12789,12790,12791,12792,12793,12794,12795,12796,12797,12798,12799,13312,13313,13314,13315,13316,13317,13318,13319,13320,13321,13322,13323,13324,13325,13326,13327,13328,13329,13330,13331,13332,13333,13334,13335,13336,13337,13338,13339,13340,13341,13342,13343,13344,13345,13346,13347,13348,13349,13350,13351,13352,13353,13354,13355,13356,13357,13358,13359,13360,13361,13362,13363,13364,13365,13366,13367,13368,13369,13370,13371,13372,13373,13374,13375,13376,13377,13378,13379,13380,13381,13382,13383,13384,13385,13386,13387,13388,13389,13390,13391,13392,13393,13394,13395,13396,13397,13398,13399,13400,13401,13402,13403,13404,13405,13406,13407,13408,13409,13410,13411,13412,13413,13414,13415,13416,13417,13418,13419,13420,13421,13422,13423,13424,13425,13426,13427,13428,13429,13430,13431,13432,13433,13434,13435,13436,13437,13438,13439,13440,13441,13442,13443,13444,13445,13446,13447,13448,13449,13450,13451,13452,13453,13454,13455,13456,13457,13458,13459,13460,13461,13462,13463,13464,13465,13466,13467,13468,13469,13470,13471,13472,13473,13474,13475,13476,13477,13478,13479,13480,13481,13482,13483,13484,13485,13486,13487,13488,13489,13490,13491,13492,13493,13494,13495,13496,13497,13498,13499,13500,13501,13502,13503,13504,13505,13506,13507,13508,13509,13510,13511,13512,13513,13514,13515,13516,13517,13518,13519,13520,13521,13522,13523,13524,13525,13526,13527,13528,13529,13530,13531,13532,13533,13534,13535,13536,13537,13538,13539,13540,13541,13542,13543,13544,13545,13546,13547,13548,13549,13550,13551,13552,13553,13554,13555,13556,13557,13558,13559,13560,13561,13562,13563,13564,13565,13566,13567,13568,13569,13570,13571,13572,13573,13574,13575,13576,13577,13578,13579,13580,13581,13582,13583,13584,13585,13586,13587,13588,13589,13590,13591,13592,13593,13594,13595,13596,13597,13598,13599,13600,13601,13602,13603,13604,13605,13606,13607,13608,13609,13610,13611,13612,13613,13614,13615,13616,13617,13618,13619,13620,13621,13622,13623,13624,13625,13626,13627,13628,13629,13630,13631,13632,13633,13634,13635,13636,13637,13638,13639,13640,13641,13642,13643,13644,13645,13646,13647,13648,13649,13650,13651,13652,13653,13654,13655,13656,13657,13658,13659,13660,13661,13662,13663,13664,13665,13666,13667,13668,13669,13670,13671,13672,13673,13674,13675,13676,13677,13678,13679,13680,13681,13682,13683,13684,13685,13686,13687,13688,13689,13690,13691,13692,13693,13694,13695,13696,13697,13698,13699,13700,13701,13702,13703,13704,13705,13706,13707,13708,13709,13710,13711,13712,13713,13714,13715,13716,13717,13718,13719,13720,13721,13722,13723,13724,13725,13726,13727,13728,13729,13730,13731,13732,13733,13734,13735,13736,13737,13738,13739,13740,13741,13742,13743,13744,13745,13746,13747,13748,13749,13750,13751,13752,13753,13754,13755,13756,13757,13758,13759,13760,13761,13762,13763,13764,13765,13766,13767,13768,13769,13770,13771,13772,13773,13774,13775,13776,13777,13778,13779,13780,13781,13782,13783,13784,13785,13786,13787,13788,13789,13790,13791,13792,13793,13794,13795,13796,13797,13798,13799,13800,13801,13802,13803,13804,13805,13806,13807,13808,13809,13810,13811,13812,13813,13814,13815,13816,13817,13818,13819,13820,13821,13822,13823,13824,13825,13826,13827,13828,13829,13830,13831,13832,13833,13834,13835,13836,13837,13838,13839,13840,13841,13842,13843,13844,13845,13846,13847,13848,13849,13850,13851,13852,13853,13854,13855,13856,13857,13858,13859,13860,13861,13862,13863,13864,13865,13866,13867,13868,13869,13870,13871,13872,13873,13874,13875,13876,13877,13878,13879,13880,13881,13882,13883,13884,13885,13886,13887,13888,13889,13890,13891,13892,13893,13894,13895,13896,13897,13898,13899,13900,13901,13902,13903,13904,13905,13906,13907,13908,13909,13910,13911,13912,13913,13914,13915,13916,13917,13918,13919,13920,13921,13922,13923,13924,13925,13926,13927,13928,13929,13930,13931,13932,13933,13934,13935,13936,13937,13938,13939,13940,13941,13942,13943,13944,13945,13946,13947,13948,13949,13950,13951,13952,13953,13954,13955,13956,13957,13958,13959,13960,13961,13962,13963,13964,13965,13966,13967,13968,13969,13970,13971,13972,13973,13974,13975,13976,13977,13978,13979,13980,13981,13982,13983,13984,13985,13986,13987,13988,13989,13990,13991,13992,13993,13994,13995,13996,13997,13998,13999,14000,14001,14002,14003,14004,14005,14006,14007,14008,14009,14010,14011,14012,14013,14014,14015,14016,14017,14018,14019,14020,14021,14022,14023,14024,14025,14026,14027,14028,14029,14030,14031,14032,14033,14034,14035,14036,14037,14038,14039,14040,14041,14042,14043,14044,14045,14046,14047,14048,14049,14050,14051,14052,14053,14054,14055,14056,14057,14058,14059,14060,14061,14062,14063,14064,14065,14066,14067,14068,14069,14070,14071,14072,14073,14074,14075,14076,14077,14078,14079,14080,14081,14082,14083,14084,14085,14086,14087,14088,14089,14090,14091,14092,14093,14094,14095,14096,14097,14098,14099,14100,14101,14102,14103,14104,14105,14106,14107,14108,14109,14110,14111,14112,14113,14114,14115,14116,14117,14118,14119,14120,14121,14122,14123,14124,14125,14126,14127,14128,14129,14130,14131,14132,14133,14134,14135,14136,14137,14138,14139,14140,14141,14142,14143,14144,14145,14146,14147,14148,14149,14150,14151,14152,14153,14154,14155,14156,14157,14158,14159,14160,14161,14162,14163,14164,14165,14166,14167,14168,14169,14170,14171,14172,14173,14174,14175,14176,14177,14178,14179,14180,14181,14182,14183,14184,14185,14186,14187,14188,14189,14190,14191,14192,14193,14194,14195,14196,14197,14198,14199,14200,14201,14202,14203,14204,14205,14206,14207,14208,14209,14210,14211,14212,14213,14214,14215,14216,14217,14218,14219,14220,14221,14222,14223,14224,14225,14226,14227,14228,14229,14230,14231,14232,14233,14234,14235,14236,14237,14238,14239,14240,14241,14242,14243,14244,14245,14246,14247,14248,14249,14250,14251,14252,14253,14254,14255,14256,14257,14258,14259,14260,14261,14262,14263,14264,14265,14266,14267,14268,14269,14270,14271,14272,14273,14274,14275,14276,14277,14278,14279,14280,14281,14282,14283,14284,14285,14286,14287,14288,14289,14290,14291,14292,14293,14294,14295,14296,14297,14298,14299,14300,14301,14302,14303,14304,14305,14306,14307,14308,14309,14310,14311,14312,14313,14314,14315,14316,14317,14318,14319,14320,14321,14322,14323,14324,14325,14326,14327,14328,14329,14330,14331,14332,14333,14334,14335,14336,14337,14338,14339,14340,14341,14342,14343,14344,14345,14346,14347,14348,14349,14350,14351,14352,14353,14354,14355,14356,14357,14358,14359,14360,14361,14362,14363,14364,14365,14366,14367,14368,14369,14370,14371,14372,14373,14374,14375,14376,14377,14378,14379,14380,14381,14382,14383,14384,14385,14386,14387,14388,14389,14390,14391,14392,14393,14394,14395,14396,14397,14398,14399,14400,14401,14402,14403,14404,14405,14406,14407,14408,14409,14410,14411,14412,14413,14414,14415,14416,14417,14418,14419,14420,14421,14422,14423,14424,14425,14426,14427,14428,14429,14430,14431,14432,14433,14434,14435,14436,14437,14438,14439,14440,14441,14442,14443,14444,14445,14446,14447,14448,14449,14450,14451,14452,14453,14454,14455,14456,14457,14458,14459,14460,14461,14462,14463,14464,14465,14466,14467,14468,14469,14470,14471,14472,14473,14474,14475,14476,14477,14478,14479,14480,14481,14482,14483,14484,14485,14486,14487,14488,14489,14490,14491,14492,14493,14494,14495,14496,14497,14498,14499,14500,14501,14502,14503,14504,14505,14506,14507,14508,14509,14510,14511,14512,14513,14514,14515,14516,14517,14518,14519,14520,14521,14522,14523,14524,14525,14526,14527,14528,14529,14530,14531,14532,14533,14534,14535,14536,14537,14538,14539,14540,14541,14542,14543,14544,14545,14546,14547,14548,14549,14550,14551,14552,14553,14554,14555,14556,14557,14558,14559,14560,14561,14562,14563,14564,14565,14566,14567,14568,14569,14570,14571,14572,14573,14574,14575,14576,14577,14578,14579,14580,14581,14582,14583,14584,14585,14586,14587,14588,14589,14590,14591,14592,14593,14594,14595,14596,14597,14598,14599,14600,14601,14602,14603,14604,14605,14606,14607,14608,14609,14610,14611,14612,14613,14614,14615,14616,14617,14618,14619,14620,14621,14622,14623,14624,14625,14626,14627,14628,14629,14630,14631,14632,14633,14634,14635,14636,14637,14638,14639,14640,14641,14642,14643,14644,14645,14646,14647,14648,14649,14650,14651,14652,14653,14654,14655,14656,14657,14658,14659,14660,14661,14662,14663,14664,14665,14666,14667,14668,14669,14670,14671,14672,14673,14674,14675,14676,14677,14678,14679,14680,14681,14682,14683,14684,14685,14686,14687,14688,14689,14690,14691,14692,14693,14694,14695,14696,14697,14698,14699,14700,14701,14702,14703,14704,14705,14706,14707,14708,14709,14710,14711,14712,14713,14714,14715,14716,14717,14718,14719,14720,14721,14722,14723,14724,14725,14726,14727,14728,14729,14730,14731,14732,14733,14734,14735,14736,14737,14738,14739,14740,14741,14742,14743,14744,14745,14746,14747,14748,14749,14750,14751,14752,14753,14754,14755,14756,14757,14758,14759,14760,14761,14762,14763,14764,14765,14766,14767,14768,14769,14770,14771,14772,14773,14774,14775,14776,14777,14778,14779,14780,14781,14782,14783,14784,14785,14786,14787,14788,14789,14790,14791,14792,14793,14794,14795,14796,14797,14798,14799,14800,14801,14802,14803,14804,14805,14806,14807,14808,14809,14810,14811,14812,14813,14814,14815,14816,14817,14818,14819,14820,14821,14822,14823,14824,14825,14826,14827,14828,14829,14830,14831,14832,14833,14834,14835,14836,14837,14838,14839,14840,14841,14842,14843,14844,14845,14846,14847,14848,14849,14850,14851,14852,14853,14854,14855,14856,14857,14858,14859,14860,14861,14862,14863,14864,14865,14866,14867,14868,14869,14870,14871,14872,14873,14874,14875,14876,14877,14878,14879,14880,14881,14882,14883,14884,14885,14886,14887,14888,14889,14890,14891,14892,14893,14894,14895,14896,14897,14898,14899,14900,14901,14902,14903,14904,14905,14906,14907,14908,14909,14910,14911,14912,14913,14914,14915,14916,14917,14918,14919,14920,14921,14922,14923,14924,14925,14926,14927,14928,14929,14930,14931,14932,14933,14934,14935,14936,14937,14938,14939,14940,14941,14942,14943,14944,14945,14946,14947,14948,14949,14950,14951,14952,14953,14954,14955,14956,14957,14958,14959,14960,14961,14962,14963,14964,14965,14966,14967,14968,14969,14970,14971,14972,14973,14974,14975,14976,14977,14978,14979,14980,14981,14982,14983,14984,14985,14986,14987,14988,14989,14990,14991,14992,14993,14994,14995,14996,14997,14998,14999,15000,15001,15002,15003,15004,15005,15006,15007,15008,15009,15010,15011,15012,15013,15014,15015,15016,15017,15018,15019,15020,15021,15022,15023,15024,15025,15026,15027,15028,15029,15030,15031,15032,15033,15034,15035,15036,15037,15038,15039,15040,15041,15042,15043,15044,15045,15046,15047,15048,15049,15050,15051,15052,15053,15054,15055,15056,15057,15058,15059,15060,15061,15062,15063,15064,15065,15066,15067,15068,15069,15070,15071,15072,15073,15074,15075,15076,15077,15078,15079,15080,15081,15082,15083,15084,15085,15086,15087,15088,15089,15090,15091,15092,15093,15094,15095,15096,15097,15098,15099,15100,15101,15102,15103,15104,15105,15106,15107,15108,15109,15110,15111,15112,15113,15114,15115,15116,15117,15118,15119,15120,15121,15122,15123,15124,15125,15126,15127,15128,15129,15130,15131,15132,15133,15134,15135,15136,15137,15138,15139,15140,15141,15142,15143,15144,15145,15146,15147,15148,15149,15150,15151,15152,15153,15154,15155,15156,15157,15158,15159,15160,15161,15162,15163,15164,15165,15166,15167,15168,15169,15170,15171,15172,15173,15174,15175,15176,15177,15178,15179,15180,15181,15182,15183,15184,15185,15186,15187,15188,15189,15190,15191,15192,15193,15194,15195,15196,15197,15198,15199,15200,15201,15202,15203,15204,15205,15206,15207,15208,15209,15210,15211,15212,15213,15214,15215,15216,15217,15218,15219,15220,15221,15222,15223,15224,15225,15226,15227,15228,15229,15230,15231,15232,15233,15234,15235,15236,15237,15238,15239,15240,15241,15242,15243,15244,15245,15246,15247,15248,15249,15250,15251,15252,15253,15254,15255,15256,15257,15258,15259,15260,15261,15262,15263,15264,15265,15266,15267,15268,15269,15270,15271,15272,15273,15274,15275,15276,15277,15278,15279,15280,15281,15282,15283,15284,15285,15286,15287,15288,15289,15290,15291,15292,15293,15294,15295,15296,15297,15298,15299,15300,15301,15302,15303,15304,15305,15306,15307,15308,15309,15310,15311,15312,15313,15314,15315,15316,15317,15318,15319,15320,15321,15322,15323,15324,15325,15326,15327,15328,15329,15330,15331,15332,15333,15334,15335,15336,15337,15338,15339,15340,15341,15342,15343,15344,15345,15346,15347,15348,15349,15350,15351,15352,15353,15354,15355,15356,15357,15358,15359,15360,15361,15362,15363,15364,15365,15366,15367,15368,15369,15370,15371,15372,15373,15374,15375,15376,15377,15378,15379,15380,15381,15382,15383,15384,15385,15386,15387,15388,15389,15390,15391,15392,15393,15394,15395,15396,15397,15398,15399,15400,15401,15402,15403,15404,15405,15406,15407,15408,15409,15410,15411,15412,15413,15414,15415,15416,15417,15418,15419,15420,15421,15422,15423,15424,15425,15426,15427,15428,15429,15430,15431,15432,15433,15434,15435,15436,15437,15438,15439,15440,15441,15442,15443,15444,15445,15446,15447,15448,15449,15450,15451,15452,15453,15454,15455,15456,15457,15458,15459,15460,15461,15462,15463,15464,15465,15466,15467,15468,15469,15470,15471,15472,15473,15474,15475,15476,15477,15478,15479,15480,15481,15482,15483,15484,15485,15486,15487,15488,15489,15490,15491,15492,15493,15494,15495,15496,15497,15498,15499,15500,15501,15502,15503,15504,15505,15506,15507,15508,15509,15510,15511,15512,15513,15514,15515,15516,15517,15518,15519,15520,15521,15522,15523,15524,15525,15526,15527,15528,15529,15530,15531,15532,15533,15534,15535,15536,15537,15538,15539,15540,15541,15542,15543,15544,15545,15546,15547,15548,15549,15550,15551,15552,15553,15554,15555,15556,15557,15558,15559,15560,15561,15562,15563,15564,15565,15566,15567,15568,15569,15570,15571,15572,15573,15574,15575,15576,15577,15578,15579,15580,15581,15582,15583,15584,15585,15586,15587,15588,15589,15590,15591,15592,15593,15594,15595,15596,15597,15598,15599,15600,15601,15602,15603,15604,15605,15606,15607,15608,15609,15610,15611,15612,15613,15614,15615,15616,15617,15618,15619,15620,15621,15622,15623,15624,15625,15626,15627,15628,15629,15630,15631,15632,15633,15634,15635,15636,15637,15638,15639,15640,15641,15642,15643,15644,15645,15646,15647,15648,15649,15650,15651,15652,15653,15654,15655,15656,15657,15658,15659,15660,15661,15662,15663,15664,15665,15666,15667,15668,15669,15670,15671,15672,15673,15674,15675,15676,15677,15678,15679,15680,15681,15682,15683,15684,15685,15686,15687,15688,15689,15690,15691,15692,15693,15694,15695,15696,15697,15698,15699,15700,15701,15702,15703,15704,15705,15706,15707,15708,15709,15710,15711,15712,15713,15714,15715,15716,15717,15718,15719,15720,15721,15722,15723,15724,15725,15726,15727,15728,15729,15730,15731,15732,15733,15734,15735,15736,15737,15738,15739,15740,15741,15742,15743,15744,15745,15746,15747,15748,15749,15750,15751,15752,15753,15754,15755,15756,15757,15758,15759,15760,15761,15762,15763,15764,15765,15766,15767,15768,15769,15770,15771,15772,15773,15774,15775,15776,15777,15778,15779,15780,15781,15782,15783,15784,15785,15786,15787,15788,15789,15790,15791,15792,15793,15794,15795,15796,15797,15798,15799,15800,15801,15802,15803,15804,15805,15806,15807,15808,15809,15810,15811,15812,15813,15814,15815,15816,15817,15818,15819,15820,15821,15822,15823,15824,15825,15826,15827,15828,15829,15830,15831,15832,15833,15834,15835,15836,15837,15838,15839,15840,15841,15842,15843,15844,15845,15846,15847,15848,15849,15850,15851,15852,15853,15854,15855,15856,15857,15858,15859,15860,15861,15862,15863,15864,15865,15866,15867,15868,15869,15870,15871,15872,15873,15874,15875,15876,15877,15878,15879,15880,15881,15882,15883,15884,15885,15886,15887,15888,15889,15890,15891,15892,15893,15894,15895,15896,15897,15898,15899,15900,15901,15902,15903,15904,15905,15906,15907,15908,15909,15910,15911,15912,15913,15914,15915,15916,15917,15918,15919,15920,15921,15922,15923,15924,15925,15926,15927,15928,15929,15930,15931,15932,15933,15934,15935,15936,15937,15938,15939,15940,15941,15942,15943,15944,15945,15946,15947,15948,15949,15950,15951,15952,15953,15954,15955,15956,15957,15958,15959,15960,15961,15962,15963,15964,15965,15966,15967,15968,15969,15970,15971,15972,15973,15974,15975,15976,15977,15978,15979,15980,15981,15982,15983,15984,15985,15986,15987,15988,15989,15990,15991,15992,15993,15994,15995,15996,15997,15998,15999,16000,16001,16002,16003,16004,16005,16006,16007,16008,16009,16010,16011,16012,16013,16014,16015,16016,16017,16018,16019,16020,16021,16022,16023,16024,16025,16026,16027,16028,16029,16030,16031,16032,16033,16034,16035,16036,16037,16038,16039,16040,16041,16042,16043,16044,16045,16046,16047,16048,16049,16050,16051,16052,16053,16054,16055,16056,16057,16058,16059,16060,16061,16062,16063,16064,16065,16066,16067,16068,16069,16070,16071,16072,16073,16074,16075,16076,16077,16078,16079,16080,16081,16082,16083,16084,16085,16086,16087,16088,16089,16090,16091,16092,16093,16094,16095,16096,16097,16098,16099,16100,16101,16102,16103,16104,16105,16106,16107,16108,16109,16110,16111,16112,16113,16114,16115,16116,16117,16118,16119,16120,16121,16122,16123,16124,16125,16126,16127,16128,16129,16130,16131,16132,16133,16134,16135,16136,16137,16138,16139,16140,16141,16142,16143,16144,16145,16146,16147,16148,16149,16150,16151,16152,16153,16154,16155,16156,16157,16158,16159,16160,16161,16162,16163,16164,16165,16166,16167,16168,16169,16170,16171,16172,16173,16174,16175,16176,16177,16178,16179,16180,16181,16182,16183,16184,16185,16186,16187,16188,16189,16190,16191,16192,16193,16194,16195,16196,16197,16198,16199,16200,16201,16202,16203,16204,16205,16206,16207,16208,16209,16210,16211,16212,16213,16214,16215,16216,16217,16218,16219,16220,16221,16222,16223,16224,16225,16226,16227,16228,16229,16230,16231,16232,16233,16234,16235,16236,16237,16238,16239,16240,16241,16242,16243,16244,16245,16246,16247,16248,16249,16250,16251,16252,16253,16254,16255,16256,16257,16258,16259,16260,16261,16262,16263,16264,16265,16266,16267,16268,16269,16270,16271,16272,16273,16274,16275,16276,16277,16278,16279,16280,16281,16282,16283,16284,16285,16286,16287,16288,16289,16290,16291,16292,16293,16294,16295,16296,16297,16298,16299,16300,16301,16302,16303,16304,16305,16306,16307,16308,16309,16310,16311,16312,16313,16314,16315,16316,16317,16318,16319,16320,16321,16322,16323,16324,16325,16326,16327,16328,16329,16330,16331,16332,16333,16334,16335,16336,16337,16338,16339,16340,16341,16342,16343,16344,16345,16346,16347,16348,16349,16350,16351,16352,16353,16354,16355,16356,16357,16358,16359,16360,16361,16362,16363,16364,16365,16366,16367,16368,16369,16370,16371,16372,16373,16374,16375,16376,16377,16378,16379,16380,16381,16382,16383,16384,16385,16386,16387,16388,16389,16390,16391,16392,16393,16394,16395,16396,16397,16398,16399,16400,16401,16402,16403,16404,16405,16406,16407,16408,16409,16410,16411,16412,16413,16414,16415,16416,16417,16418,16419,16420,16421,16422,16423,16424,16425,16426,16427,16428,16429,16430,16431,16432,16433,16434,16435,16436,16437,16438,16439,16440,16441,16442,16443,16444,16445,16446,16447,16448,16449,16450,16451,16452,16453,16454,16455,16456,16457,16458,16459,16460,16461,16462,16463,16464,16465,16466,16467,16468,16469,16470,16471,16472,16473,16474,16475,16476,16477,16478,16479,16480,16481,16482,16483,16484,16485,16486,16487,16488,16489,16490,16491,16492,16493,16494,16495,16496,16497,16498,16499,16500,16501,16502,16503,16504,16505,16506,16507,16508,16509,16510,16511,16512,16513,16514,16515,16516,16517,16518,16519,16520,16521,16522,16523,16524,16525,16526,16527,16528,16529,16530,16531,16532,16533,16534,16535,16536,16537,16538,16539,16540,16541,16542,16543,16544,16545,16546,16547,16548,16549,16550,16551,16552,16553,16554,16555,16556,16557,16558,16559,16560,16561,16562,16563,16564,16565,16566,16567,16568,16569,16570,16571,16572,16573,16574,16575,16576,16577,16578,16579,16580,16581,16582,16583,16584,16585,16586,16587,16588,16589,16590,16591,16592,16593,16594,16595,16596,16597,16598,16599,16600,16601,16602,16603,16604,16605,16606,16607,16608,16609,16610,16611,16612,16613,16614,16615,16616,16617,16618,16619,16620,16621,16622,16623,16624,16625,16626,16627,16628,16629,16630,16631,16632,16633,16634,16635,16636,16637,16638,16639,16640,16641,16642,16643,16644,16645,16646,16647,16648,16649,16650,16651,16652,16653,16654,16655,16656,16657,16658,16659,16660,16661,16662,16663,16664,16665,16666,16667,16668,16669,16670,16671,16672,16673,16674,16675,16676,16677,16678,16679,16680,16681,16682,16683,16684,16685,16686,16687,16688,16689,16690,16691,16692,16693,16694,16695,16696,16697,16698,16699,16700,16701,16702,16703,16704,16705,16706,16707,16708,16709,16710,16711,16712,16713,16714,16715,16716,16717,16718,16719,16720,16721,16722,16723,16724,16725,16726,16727,16728,16729,16730,16731,16732,16733,16734,16735,16736,16737,16738,16739,16740,16741,16742,16743,16744,16745,16746,16747,16748,16749,16750,16751,16752,16753,16754,16755,16756,16757,16758,16759,16760,16761,16762,16763,16764,16765,16766,16767,16768,16769,16770,16771,16772,16773,16774,16775,16776,16777,16778,16779,16780,16781,16782,16783,16784,16785,16786,16787,16788,16789,16790,16791,16792,16793,16794,16795,16796,16797,16798,16799,16800,16801,16802,16803,16804,16805,16806,16807,16808,16809,16810,16811,16812,16813,16814,16815,16816,16817,16818,16819,16820,16821,16822,16823,16824,16825,16826,16827,16828,16829,16830,16831,16832,16833,16834,16835,16836,16837,16838,16839,16840,16841,16842,16843,16844,16845,16846,16847,16848,16849,16850,16851,16852,16853,16854,16855,16856,16857,16858,16859,16860,16861,16862,16863,16864,16865,16866,16867,16868,16869,16870,16871,16872,16873,16874,16875,16876,16877,16878,16879,16880,16881,16882,16883,16884,16885,16886,16887,16888,16889,16890,16891,16892,16893,16894,16895,16896,16897,16898,16899,16900,16901,16902,16903,16904,16905,16906,16907,16908,16909,16910,16911,16912,16913,16914,16915,16916,16917,16918,16919,16920,16921,16922,16923,16924,16925,16926,16927,16928,16929,16930,16931,16932,16933,16934,16935,16936,16937,16938,16939,16940,16941,16942,16943,16944,16945,16946,16947,16948,16949,16950,16951,16952,16953,16954,16955,16956,16957,16958,16959,16960,16961,16962,16963,16964,16965,16966,16967,16968,16969,16970,16971,16972,16973,16974,16975,16976,16977,16978,16979,16980,16981,16982,16983,16984,16985,16986,16987,16988,16989,16990,16991,16992,16993,16994,16995,16996,16997,16998,16999,17000,17001,17002,17003,17004,17005,17006,17007,17008,17009,17010,17011,17012,17013,17014,17015,17016,17017,17018,17019,17020,17021,17022,17023,17024,17025,17026,17027,17028,17029,17030,17031,17032,17033,17034,17035,17036,17037,17038,17039,17040,17041,17042,17043,17044,17045,17046,17047,17048,17049,17050,17051,17052,17053,17054,17055,17056,17057,17058,17059,17060,17061,17062,17063,17064,17065,17066,17067,17068,17069,17070,17071,17072,17073,17074,17075,17076,17077,17078,17079,17080,17081,17082,17083,17084,17085,17086,17087,17088,17089,17090,17091,17092,17093,17094,17095,17096,17097,17098,17099,17100,17101,17102,17103,17104,17105,17106,17107,17108,17109,17110,17111,17112,17113,17114,17115,17116,17117,17118,17119,17120,17121,17122,17123,17124,17125,17126,17127,17128,17129,17130,17131,17132,17133,17134,17135,17136,17137,17138,17139,17140,17141,17142,17143,17144,17145,17146,17147,17148,17149,17150,17151,17152,17153,17154,17155,17156,17157,17158,17159,17160,17161,17162,17163,17164,17165,17166,17167,17168,17169,17170,17171,17172,17173,17174,17175,17176,17177,17178,17179,17180,17181,17182,17183,17184,17185,17186,17187,17188,17189,17190,17191,17192,17193,17194,17195,17196,17197,17198,17199,17200,17201,17202,17203,17204,17205,17206,17207,17208,17209,17210,17211,17212,17213,17214,17215,17216,17217,17218,17219,17220,17221,17222,17223,17224,17225,17226,17227,17228,17229,17230,17231,17232,17233,17234,17235,17236,17237,17238,17239,17240,17241,17242,17243,17244,17245,17246,17247,17248,17249,17250,17251,17252,17253,17254,17255,17256,17257,17258,17259,17260,17261,17262,17263,17264,17265,17266,17267,17268,17269,17270,17271,17272,17273,17274,17275,17276,17277,17278,17279,17280,17281,17282,17283,17284,17285,17286,17287,17288,17289,17290,17291,17292,17293,17294,17295,17296,17297,17298,17299,17300,17301,17302,17303,17304,17305,17306,17307,17308,17309,17310,17311,17312,17313,17314,17315,17316,17317,17318,17319,17320,17321,17322,17323,17324,17325,17326,17327,17328,17329,17330,17331,17332,17333,17334,17335,17336,17337,17338,17339,17340,17341,17342,17343,17344,17345,17346,17347,17348,17349,17350,17351,17352,17353,17354,17355,17356,17357,17358,17359,17360,17361,17362,17363,17364,17365,17366,17367,17368,17369,17370,17371,17372,17373,17374,17375,17376,17377,17378,17379,17380,17381,17382,17383,17384,17385,17386,17387,17388,17389,17390,17391,17392,17393,17394,17395,17396,17397,17398,17399,17400,17401,17402,17403,17404,17405,17406,17407,17408,17409,17410,17411,17412,17413,17414,17415,17416,17417,17418,17419,17420,17421,17422,17423,17424,17425,17426,17427,17428,17429,17430,17431,17432,17433,17434,17435,17436,17437,17438,17439,17440,17441,17442,17443,17444,17445,17446,17447,17448,17449,17450,17451,17452,17453,17454,17455,17456,17457,17458,17459,17460,17461,17462,17463,17464,17465,17466,17467,17468,17469,17470,17471,17472,17473,17474,17475,17476,17477,17478,17479,17480,17481,17482,17483,17484,17485,17486,17487,17488,17489,17490,17491,17492,17493,17494,17495,17496,17497,17498,17499,17500,17501,17502,17503,17504,17505,17506,17507,17508,17509,17510,17511,17512,17513,17514,17515,17516,17517,17518,17519,17520,17521,17522,17523,17524,17525,17526,17527,17528,17529,17530,17531,17532,17533,17534,17535,17536,17537,17538,17539,17540,17541,17542,17543,17544,17545,17546,17547,17548,17549,17550,17551,17552,17553,17554,17555,17556,17557,17558,17559,17560,17561,17562,17563,17564,17565,17566,17567,17568,17569,17570,17571,17572,17573,17574,17575,17576,17577,17578,17579,17580,17581,17582,17583,17584,17585,17586,17587,17588,17589,17590,17591,17592,17593,17594,17595,17596,17597,17598,17599,17600,17601,17602,17603,17604,17605,17606,17607,17608,17609,17610,17611,17612,17613,17614,17615,17616,17617,17618,17619,17620,17621,17622,17623,17624,17625,17626,17627,17628,17629,17630,17631,17632,17633,17634,17635,17636,17637,17638,17639,17640,17641,17642,17643,17644,17645,17646,17647,17648,17649,17650,17651,17652,17653,17654,17655,17656,17657,17658,17659,17660,17661,17662,17663,17664,17665,17666,17667,17668,17669,17670,17671,17672,17673,17674,17675,17676,17677,17678,17679,17680,17681,17682,17683,17684,17685,17686,17687,17688,17689,17690,17691,17692,17693,17694,17695,17696,17697,17698,17699,17700,17701,17702,17703,17704,17705,17706,17707,17708,17709,17710,17711,17712,17713,17714,17715,17716,17717,17718,17719,17720,17721,17722,17723,17724,17725,17726,17727,17728,17729,17730,17731,17732,17733,17734,17735,17736,17737,17738,17739,17740,17741,17742,17743,17744,17745,17746,17747,17748,17749,17750,17751,17752,17753,17754,17755,17756,17757,17758,17759,17760,17761,17762,17763,17764,17765,17766,17767,17768,17769,17770,17771,17772,17773,17774,17775,17776,17777,17778,17779,17780,17781,17782,17783,17784,17785,17786,17787,17788,17789,17790,17791,17792,17793,17794,17795,17796,17797,17798,17799,17800,17801,17802,17803,17804,17805,17806,17807,17808,17809,17810,17811,17812,17813,17814,17815,17816,17817,17818,17819,17820,17821,17822,17823,17824,17825,17826,17827,17828,17829,17830,17831,17832,17833,17834,17835,17836,17837,17838,17839,17840,17841,17842,17843,17844,17845,17846,17847,17848,17849,17850,17851,17852,17853,17854,17855,17856,17857,17858,17859,17860,17861,17862,17863,17864,17865,17866,17867,17868,17869,17870,17871,17872,17873,17874,17875,17876,17877,17878,17879,17880,17881,17882,17883,17884,17885,17886,17887,17888,17889,17890,17891,17892,17893,17894,17895,17896,17897,17898,17899,17900,17901,17902,17903,17904,17905,17906,17907,17908,17909,17910,17911,17912,17913,17914,17915,17916,17917,17918,17919,17920,17921,17922,17923,17924,17925,17926,17927,17928,17929,17930,17931,17932,17933,17934,17935,17936,17937,17938,17939,17940,17941,17942,17943,17944,17945,17946,17947,17948,17949,17950,17951,17952,17953,17954,17955,17956,17957,17958,17959,17960,17961,17962,17963,17964,17965,17966,17967,17968,17969,17970,17971,17972,17973,17974,17975,17976,17977,17978,17979,17980,17981,17982,17983,17984,17985,17986,17987,17988,17989,17990,17991,17992,17993,17994,17995,17996,17997,17998,17999,18000,18001,18002,18003,18004,18005,18006,18007,18008,18009,18010,18011,18012,18013,18014,18015,18016,18017,18018,18019,18020,18021,18022,18023,18024,18025,18026,18027,18028,18029,18030,18031,18032,18033,18034,18035,18036,18037,18038,18039,18040,18041,18042,18043,18044,18045,18046,18047,18048,18049,18050,18051,18052,18053,18054,18055,18056,18057,18058,18059,18060,18061,18062,18063,18064,18065,18066,18067,18068,18069,18070,18071,18072,18073,18074,18075,18076,18077,18078,18079,18080,18081,18082,18083,18084,18085,18086,18087,18088,18089,18090,18091,18092,18093,18094,18095,18096,18097,18098,18099,18100,18101,18102,18103,18104,18105,18106,18107,18108,18109,18110,18111,18112,18113,18114,18115,18116,18117,18118,18119,18120,18121,18122,18123,18124,18125,18126,18127,18128,18129,18130,18131,18132,18133,18134,18135,18136,18137,18138,18139,18140,18141,18142,18143,18144,18145,18146,18147,18148,18149,18150,18151,18152,18153,18154,18155,18156,18157,18158,18159,18160,18161,18162,18163,18164,18165,18166,18167,18168,18169,18170,18171,18172,18173,18174,18175,18176,18177,18178,18179,18180,18181,18182,18183,18184,18185,18186,18187,18188,18189,18190,18191,18192,18193,18194,18195,18196,18197,18198,18199,18200,18201,18202,18203,18204,18205,18206,18207,18208,18209,18210,18211,18212,18213,18214,18215,18216,18217,18218,18219,18220,18221,18222,18223,18224,18225,18226,18227,18228,18229,18230,18231,18232,18233,18234,18235,18236,18237,18238,18239,18240,18241,18242,18243,18244,18245,18246,18247,18248,18249,18250,18251,18252,18253,18254,18255,18256,18257,18258,18259,18260,18261,18262,18263,18264,18265,18266,18267,18268,18269,18270,18271,18272,18273,18274,18275,18276,18277,18278,18279,18280,18281,18282,18283,18284,18285,18286,18287,18288,18289,18290,18291,18292,18293,18294,18295,18296,18297,18298,18299,18300,18301,18302,18303,18304,18305,18306,18307,18308,18309,18310,18311,18312,18313,18314,18315,18316,18317,18318,18319,18320,18321,18322,18323,18324,18325,18326,18327,18328,18329,18330,18331,18332,18333,18334,18335,18336,18337,18338,18339,18340,18341,18342,18343,18344,18345,18346,18347,18348,18349,18350,18351,18352,18353,18354,18355,18356,18357,18358,18359,18360,18361,18362,18363,18364,18365,18366,18367,18368,18369,18370,18371,18372,18373,18374,18375,18376,18377,18378,18379,18380,18381,18382,18383,18384,18385,18386,18387,18388,18389,18390,18391,18392,18393,18394,18395,18396,18397,18398,18399,18400,18401,18402,18403,18404,18405,18406,18407,18408,18409,18410,18411,18412,18413,18414,18415,18416,18417,18418,18419,18420,18421,18422,18423,18424,18425,18426,18427,18428,18429,18430,18431,18432,18433,18434,18435,18436,18437,18438,18439,18440,18441,18442,18443,18444,18445,18446,18447,18448,18449,18450,18451,18452,18453,18454,18455,18456,18457,18458,18459,18460,18461,18462,18463,18464,18465,18466,18467,18468,18469,18470,18471,18472,18473,18474,18475,18476,18477,18478,18479,18480,18481,18482,18483,18484,18485,18486,18487,18488,18489,18490,18491,18492,18493,18494,18495,18496,18497,18498,18499,18500,18501,18502,18503,18504,18505,18506,18507,18508,18509,18510,18511,18512,18513,18514,18515,18516,18517,18518,18519,18520,18521,18522,18523,18524,18525,18526,18527,18528,18529,18530,18531,18532,18533,18534,18535,18536,18537,18538,18539,18540,18541,18542,18543,18544,18545,18546,18547,18548,18549,18550,18551,18552,18553,18554,18555,18556,18557,18558,18559,18560,18561,18562,18563,18564,18565,18566,18567,18568,18569,18570,18571,18572,18573,18574,18575,18576,18577,18578,18579,18580,18581,18582,18583,18584,18585,18586,18587,18588,18589,18590,18591,18592,18593,18594,18595,18596,18597,18598,18599,18600,18601,18602,18603,18604,18605,18606,18607,18608,18609,18610,18611,18612,18613,18614,18615,18616,18617,18618,18619,18620,18621,18622,18623,18624,18625,18626,18627,18628,18629,18630,18631,18632,18633,18634,18635,18636,18637,18638,18639,18640,18641,18642,18643,18644,18645,18646,18647,18648,18649,18650,18651,18652,18653,18654,18655,18656,18657,18658,18659,18660,18661,18662,18663,18664,18665,18666,18667,18668,18669,18670,18671,18672,18673,18674,18675,18676,18677,18678,18679,18680,18681,18682,18683,18684,18685,18686,18687,18688,18689,18690,18691,18692,18693,18694,18695,18696,18697,18698,18699,18700,18701,18702,18703,18704,18705,18706,18707,18708,18709,18710,18711,18712,18713,18714,18715,18716,18717,18718,18719,18720,18721,18722,18723,18724,18725,18726,18727,18728,18729,18730,18731,18732,18733,18734,18735,18736,18737,18738,18739,18740,18741,18742,18743,18744,18745,18746,18747,18748,18749,18750,18751,18752,18753,18754,18755,18756,18757,18758,18759,18760,18761,18762,18763,18764,18765,18766,18767,18768,18769,18770,18771,18772,18773,18774,18775,18776,18777,18778,18779,18780,18781,18782,18783,18784,18785,18786,18787,18788,18789,18790,18791,18792,18793,18794,18795,18796,18797,18798,18799,18800,18801,18802,18803,18804,18805,18806,18807,18808,18809,18810,18811,18812,18813,18814,18815,18816,18817,18818,18819,18820,18821,18822,18823,18824,18825,18826,18827,18828,18829,18830,18831,18832,18833,18834,18835,18836,18837,18838,18839,18840,18841,18842,18843,18844,18845,18846,18847,18848,18849,18850,18851,18852,18853,18854,18855,18856,18857,18858,18859,18860,18861,18862,18863,18864,18865,18866,18867,18868,18869,18870,18871,18872,18873,18874,18875,18876,18877,18878,18879,18880,18881,18882,18883,18884,18885,18886,18887,18888,18889,18890,18891,18892,18893,18894,18895,18896,18897,18898,18899,18900,18901,18902,18903,18904,18905,18906,18907,18908,18909,18910,18911,18912,18913,18914,18915,18916,18917,18918,18919,18920,18921,18922,18923,18924,18925,18926,18927,18928,18929,18930,18931,18932,18933,18934,18935,18936,18937,18938,18939,18940,18941,18942,18943,18944,18945,18946,18947,18948,18949,18950,18951,18952,18953,18954,18955,18956,18957,18958,18959,18960,18961,18962,18963,18964,18965,18966,18967,18968,18969,18970,18971,18972,18973,18974,18975,18976,18977,18978,18979,18980,18981,18982,18983,18984,18985,18986,18987,18988,18989,18990,18991,18992,18993,18994,18995,18996,18997,18998,18999,19000,19001,19002,19003,19004,19005,19006,19007,19008,19009,19010,19011,19012,19013,19014,19015,19016,19017,19018,19019,19020,19021,19022,19023,19024,19025,19026,19027,19028,19029,19030,19031,19032,19033,19034,19035,19036,19037,19038,19039,19040,19041,19042,19043,19044,19045,19046,19047,19048,19049,19050,19051,19052,19053,19054,19055,19056,19057,19058,19059,19060,19061,19062,19063,19064,19065,19066,19067,19068,19069,19070,19071,19072,19073,19074,19075,19076,19077,19078,19079,19080,19081,19082,19083,19084,19085,19086,19087,19088,19089,19090,19091,19092,19093,19094,19095,19096,19097,19098,19099,19100,19101,19102,19103,19104,19105,19106,19107,19108,19109,19110,19111,19112,19113,19114,19115,19116,19117,19118,19119,19120,19121,19122,19123,19124,19125,19126,19127,19128,19129,19130,19131,19132,19133,19134,19135,19136,19137,19138,19139,19140,19141,19142,19143,19144,19145,19146,19147,19148,19149,19150,19151,19152,19153,19154,19155,19156,19157,19158,19159,19160,19161,19162,19163,19164,19165,19166,19167,19168,19169,19170,19171,19172,19173,19174,19175,19176,19177,19178,19179,19180,19181,19182,19183,19184,19185,19186,19187,19188,19189,19190,19191,19192,19193,19194,19195,19196,19197,19198,19199,19200,19201,19202,19203,19204,19205,19206,19207,19208,19209,19210,19211,19212,19213,19214,19215,19216,19217,19218,19219,19220,19221,19222,19223,19224,19225,19226,19227,19228,19229,19230,19231,19232,19233,19234,19235,19236,19237,19238,19239,19240,19241,19242,19243,19244,19245,19246,19247,19248,19249,19250,19251,19252,19253,19254,19255,19256,19257,19258,19259,19260,19261,19262,19263,19264,19265,19266,19267,19268,19269,19270,19271,19272,19273,19274,19275,19276,19277,19278,19279,19280,19281,19282,19283,19284,19285,19286,19287,19288,19289,19290,19291,19292,19293,19294,19295,19296,19297,19298,19299,19300,19301,19302,19303,19304,19305,19306,19307,19308,19309,19310,19311,19312,19313,19314,19315,19316,19317,19318,19319,19320,19321,19322,19323,19324,19325,19326,19327,19328,19329,19330,19331,19332,19333,19334,19335,19336,19337,19338,19339,19340,19341,19342,19343,19344,19345,19346,19347,19348,19349,19350,19351,19352,19353,19354,19355,19356,19357,19358,19359,19360,19361,19362,19363,19364,19365,19366,19367,19368,19369,19370,19371,19372,19373,19374,19375,19376,19377,19378,19379,19380,19381,19382,19383,19384,19385,19386,19387,19388,19389,19390,19391,19392,19393,19394,19395,19396,19397,19398,19399,19400,19401,19402,19403,19404,19405,19406,19407,19408,19409,19410,19411,19412,19413,19414,19415,19416,19417,19418,19419,19420,19421,19422,19423,19424,19425,19426,19427,19428,19429,19430,19431,19432,19433,19434,19435,19436,19437,19438,19439,19440,19441,19442,19443,19444,19445,19446,19447,19448,19449,19450,19451,19452,19453,19454,19455,19456,19457,19458,19459,19460,19461,19462,19463,19464,19465,19466,19467,19468,19469,19470,19471,19472,19473,19474,19475,19476,19477,19478,19479,19480,19481,19482,19483,19484,19485,19486,19487,19488,19489,19490,19491,19492,19493,19494,19495,19496,19497,19498,19499,19500,19501,19502,19503,19504,19505,19506,19507,19508,19509,19510,19511,19512,19513,19514,19515,19516,19517,19518,19519,19520,19521,19522,19523,19524,19525,19526,19527,19528,19529,19530,19531,19532,19533,19534,19535,19536,19537,19538,19539,19540,19541,19542,19543,19544,19545,19546,19547,19548,19549,19550,19551,19552,19553,19554,19555,19556,19557,19558,19559,19560,19561,19562,19563,19564,19565,19566,19567,19568,19569,19570,19571,19572,19573,19574,19575,19576,19577,19578,19579,19580,19581,19582,19583,19584,19585,19586,19587,19588,19589,19590,19591,19592,19593,19594,19595,19596,19597,19598,19599,19600,19601,19602,19603,19604,19605,19606,19607,19608,19609,19610,19611,19612,19613,19614,19615,19616,19617,19618,19619,19620,19621,19622,19623,19624,19625,19626,19627,19628,19629,19630,19631,19632,19633,19634,19635,19636,19637,19638,19639,19640,19641,19642,19643,19644,19645,19646,19647,19648,19649,19650,19651,19652,19653,19654,19655,19656,19657,19658,19659,19660,19661,19662,19663,19664,19665,19666,19667,19668,19669,19670,19671,19672,19673,19674,19675,19676,19677,19678,19679,19680,19681,19682,19683,19684,19685,19686,19687,19688,19689,19690,19691,19692,19693,19694,19695,19696,19697,19698,19699,19700,19701,19702,19703,19704,19705,19706,19707,19708,19709,19710,19711,19712,19713,19714,19715,19716,19717,19718,19719,19720,19721,19722,19723,19724,19725,19726,19727,19728,19729,19730,19731,19732,19733,19734,19735,19736,19737,19738,19739,19740,19741,19742,19743,19744,19745,19746,19747,19748,19749,19750,19751,19752,19753,19754,19755,19756,19757,19758,19759,19760,19761,19762,19763,19764,19765,19766,19767,19768,19769,19770,19771,19772,19773,19774,19775,19776,19777,19778,19779,19780,19781,19782,19783,19784,19785,19786,19787,19788,19789,19790,19791,19792,19793,19794,19795,19796,19797,19798,19799,19800,19801,19802,19803,19804,19805,19806,19807,19808,19809,19810,19811,19812,19813,19814,19815,19816,19817,19818,19819,19820,19821,19822,19823,19824,19825,19826,19827,19828,19829,19830,19831,19832,19833,19834,19835,19836,19837,19838,19839,19840,19841,19842,19843,19844,19845,19846,19847,19848,19849,19850,19851,19852,19853,19854,19855,19856,19857,19858,19859,19860,19861,19862,19863,19864,19865,19866,19867,19868,19869,19870,19871,19872,19873,19874,19875,19876,19877,19878,19879,19880,19881,19882,19883,19884,19885,19886,19887,19888,19889,19890,19891,19892,19893,19968,19969,19970,19971,19972,19973,19974,19975,19976,19977,19978,19979,19980,19981,19982,19983,19984,19985,19986,19987,19988,19989,19990,19991,19992,19993,19994,19995,19996,19997,19998,19999,20000,20001,20002,20003,20004,20005,20006,20007,20008,20009,20010,20011,20012,20013,20014,20015,20016,20017,20018,20019,20020,20021,20022,20023,20024,20025,20026,20027,20028,20029,20030,20031,20032,20033,20034,20035,20036,20037,20038,20039,20040,20041,20042,20043,20044,20045,20046,20047,20048,20049,20050,20051,20052,20053,20054,20055,20056,20057,20058,20059,20060,20061,20062,20063,20064,20065,20066,20067,20068,20069,20070,20071,20072,20073,20074,20075,20076,20077,20078,20079,20080,20081,20082,20083,20084,20085,20086,20087,20088,20089,20090,20091,20092,20093,20094,20095,20096,20097,20098,20099,20100,20101,20102,20103,20104,20105,20106,20107,20108,20109,20110,20111,20112,20113,20114,20115,20116,20117,20118,20119,20120,20121,20122,20123,20124,20125,20126,20127,20128,20129,20130,20131,20132,20133,20134,20135,20136,20137,20138,20139,20140,20141,20142,20143,20144,20145,20146,20147,20148,20149,20150,20151,20152,20153,20154,20155,20156,20157,20158,20159,20160,20161,20162,20163,20164,20165,20166,20167,20168,20169,20170,20171,20172,20173,20174,20175,20176,20177,20178,20179,20180,20181,20182,20183,20184,20185,20186,20187,20188,20189,20190,20191,20192,20193,20194,20195,20196,20197,20198,20199,20200,20201,20202,20203,20204,20205,20206,20207,20208,20209,20210,20211,20212,20213,20214,20215,20216,20217,20218,20219,20220,20221,20222,20223,20224,20225,20226,20227,20228,20229,20230,20231,20232,20233,20234,20235,20236,20237,20238,20239,20240,20241,20242,20243,20244,20245,20246,20247,20248,20249,20250,20251,20252,20253,20254,20255,20256,20257,20258,20259,20260,20261,20262,20263,20264,20265,20266,20267,20268,20269,20270,20271,20272,20273,20274,20275,20276,20277,20278,20279,20280,20281,20282,20283,20284,20285,20286,20287,20288,20289,20290,20291,20292,20293,20294,20295,20296,20297,20298,20299,20300,20301,20302,20303,20304,20305,20306,20307,20308,20309,20310,20311,20312,20313,20314,20315,20316,20317,20318,20319,20320,20321,20322,20323,20324,20325,20326,20327,20328,20329,20330,20331,20332,20333,20334,20335,20336,20337,20338,20339,20340,20341,20342,20343,20344,20345,20346,20347,20348,20349,20350,20351,20352,20353,20354,20355,20356,20357,20358,20359,20360,20361,20362,20363,20364,20365,20366,20367,20368,20369,20370,20371,20372,20373,20374,20375,20376,20377,20378,20379,20380,20381,20382,20383,20384,20385,20386,20387,20388,20389,20390,20391,20392,20393,20394,20395,20396,20397,20398,20399,20400,20401,20402,20403,20404,20405,20406,20407,20408,20409,20410,20411,20412,20413,20414,20415,20416,20417,20418,20419,20420,20421,20422,20423,20424,20425,20426,20427,20428,20429,20430,20431,20432,20433,20434,20435,20436,20437,20438,20439,20440,20441,20442,20443,20444,20445,20446,20447,20448,20449,20450,20451,20452,20453,20454,20455,20456,20457,20458,20459,20460,20461,20462,20463,20464,20465,20466,20467,20468,20469,20470,20471,20472,20473,20474,20475,20476,20477,20478,20479,20480,20481,20482,20483,20484,20485,20486,20487,20488,20489,20490,20491,20492,20493,20494,20495,20496,20497,20498,20499,20500,20501,20502,20503,20504,20505,20506,20507,20508,20509,20510,20511,20512,20513,20514,20515,20516,20517,20518,20519,20520,20521,20522,20523,20524,20525,20526,20527,20528,20529,20530,20531,20532,20533,20534,20535,20536,20537,20538,20539,20540,20541,20542,20543,20544,20545,20546,20547,20548,20549,20550,20551,20552,20553,20554,20555,20556,20557,20558,20559,20560,20561,20562,20563,20564,20565,20566,20567,20568,20569,20570,20571,20572,20573,20574,20575,20576,20577,20578,20579,20580,20581,20582,20583,20584,20585,20586,20587,20588,20589,20590,20591,20592,20593,20594,20595,20596,20597,20598,20599,20600,20601,20602,20603,20604,20605,20606,20607,20608,20609,20610,20611,20612,20613,20614,20615,20616,20617,20618,20619,20620,20621,20622,20623,20624,20625,20626,20627,20628,20629,20630,20631,20632,20633,20634,20635,20636,20637,20638,20639,20640,20641,20642,20643,20644,20645,20646,20647,20648,20649,20650,20651,20652,20653,20654,20655,20656,20657,20658,20659,20660,20661,20662,20663,20664,20665,20666,20667,20668,20669,20670,20671,20672,20673,20674,20675,20676,20677,20678,20679,20680,20681,20682,20683,20684,20685,20686,20687,20688,20689,20690,20691,20692,20693,20694,20695,20696,20697,20698,20699,20700,20701,20702,20703,20704,20705,20706,20707,20708,20709,20710,20711,20712,20713,20714,20715,20716,20717,20718,20719,20720,20721,20722,20723,20724,20725,20726,20727,20728,20729,20730,20731,20732,20733,20734,20735,20736,20737,20738,20739,20740,20741,20742,20743,20744,20745,20746,20747,20748,20749,20750,20751,20752,20753,20754,20755,20756,20757,20758,20759,20760,20761,20762,20763,20764,20765,20766,20767,20768,20769,20770,20771,20772,20773,20774,20775,20776,20777,20778,20779,20780,20781,20782,20783,20784,20785,20786,20787,20788,20789,20790,20791,20792,20793,20794,20795,20796,20797,20798,20799,20800,20801,20802,20803,20804,20805,20806,20807,20808,20809,20810,20811,20812,20813,20814,20815,20816,20817,20818,20819,20820,20821,20822,20823,20824,20825,20826,20827,20828,20829,20830,20831,20832,20833,20834,20835,20836,20837,20838,20839,20840,20841,20842,20843,20844,20845,20846,20847,20848,20849,20850,20851,20852,20853,20854,20855,20856,20857,20858,20859,20860,20861,20862,20863,20864,20865,20866,20867,20868,20869,20870,20871,20872,20873,20874,20875,20876,20877,20878,20879,20880,20881,20882,20883,20884,20885,20886,20887,20888,20889,20890,20891,20892,20893,20894,20895,20896,20897,20898,20899,20900,20901,20902,20903,20904,20905,20906,20907,20908,20909,20910,20911,20912,20913,20914,20915,20916,20917,20918,20919,20920,20921,20922,20923,20924,20925,20926,20927,20928,20929,20930,20931,20932,20933,20934,20935,20936,20937,20938,20939,20940,20941,20942,20943,20944,20945,20946,20947,20948,20949,20950,20951,20952,20953,20954,20955,20956,20957,20958,20959,20960,20961,20962,20963,20964,20965,20966,20967,20968,20969,20970,20971,20972,20973,20974,20975,20976,20977,20978,20979,20980,20981,20982,20983,20984,20985,20986,20987,20988,20989,20990,20991,20992,20993,20994,20995,20996,20997,20998,20999,21000,21001,21002,21003,21004,21005,21006,21007,21008,21009,21010,21011,21012,21013,21014,21015,21016,21017,21018,21019,21020,21021,21022,21023,21024,21025,21026,21027,21028,21029,21030,21031,21032,21033,21034,21035,21036,21037,21038,21039,21040,21041,21042,21043,21044,21045,21046,21047,21048,21049,21050,21051,21052,21053,21054,21055,21056,21057,21058,21059,21060,21061,21062,21063,21064,21065,21066,21067,21068,21069,21070,21071,21072,21073,21074,21075,21076,21077,21078,21079,21080,21081,21082,21083,21084,21085,21086,21087,21088,21089,21090,21091,21092,21093,21094,21095,21096,21097,21098,21099,21100,21101,21102,21103,21104,21105,21106,21107,21108,21109,21110,21111,21112,21113,21114,21115,21116,21117,21118,21119,21120,21121,21122,21123,21124,21125,21126,21127,21128,21129,21130,21131,21132,21133,21134,21135,21136,21137,21138,21139,21140,21141,21142,21143,21144,21145,21146,21147,21148,21149,21150,21151,21152,21153,21154,21155,21156,21157,21158,21159,21160,21161,21162,21163,21164,21165,21166,21167,21168,21169,21170,21171,21172,21173,21174,21175,21176,21177,21178,21179,21180,21181,21182,21183,21184,21185,21186,21187,21188,21189,21190,21191,21192,21193,21194,21195,21196,21197,21198,21199,21200,21201,21202,21203,21204,21205,21206,21207,21208,21209,21210,21211,21212,21213,21214,21215,21216,21217,21218,21219,21220,21221,21222,21223,21224,21225,21226,21227,21228,21229,21230,21231,21232,21233,21234,21235,21236,21237,21238,21239,21240,21241,21242,21243,21244,21245,21246,21247,21248,21249,21250,21251,21252,21253,21254,21255,21256,21257,21258,21259,21260,21261,21262,21263,21264,21265,21266,21267,21268,21269,21270,21271,21272,21273,21274,21275,21276,21277,21278,21279,21280,21281,21282,21283,21284,21285,21286,21287,21288,21289,21290,21291,21292,21293,21294,21295,21296,21297,21298,21299,21300,21301,21302,21303,21304,21305,21306,21307,21308,21309,21310,21311,21312,21313,21314,21315,21316,21317,21318,21319,21320,21321,21322,21323,21324,21325,21326,21327,21328,21329,21330,21331,21332,21333,21334,21335,21336,21337,21338,21339,21340,21341,21342,21343,21344,21345,21346,21347,21348,21349,21350,21351,21352,21353,21354,21355,21356,21357,21358,21359,21360,21361,21362,21363,21364,21365,21366,21367,21368,21369,21370,21371,21372,21373,21374,21375,21376,21377,21378,21379,21380,21381,21382,21383,21384,21385,21386,21387,21388,21389,21390,21391,21392,21393,21394,21395,21396,21397,21398,21399,21400,21401,21402,21403,21404,21405,21406,21407,21408,21409,21410,21411,21412,21413,21414,21415,21416,21417,21418,21419,21420,21421,21422,21423,21424,21425,21426,21427,21428,21429,21430,21431,21432,21433,21434,21435,21436,21437,21438,21439,21440,21441,21442,21443,21444,21445,21446,21447,21448,21449,21450,21451,21452,21453,21454,21455,21456,21457,21458,21459,21460,21461,21462,21463,21464,21465,21466,21467,21468,21469,21470,21471,21472,21473,21474,21475,21476,21477,21478,21479,21480,21481,21482,21483,21484,21485,21486,21487,21488,21489,21490,21491,21492,21493,21494,21495,21496,21497,21498,21499,21500,21501,21502,21503,21504,21505,21506,21507,21508,21509,21510,21511,21512,21513,21514,21515,21516,21517,21518,21519,21520,21521,21522,21523,21524,21525,21526,21527,21528,21529,21530,21531,21532,21533,21534,21535,21536,21537,21538,21539,21540,21541,21542,21543,21544,21545,21546,21547,21548,21549,21550,21551,21552,21553,21554,21555,21556,21557,21558,21559,21560,21561,21562,21563,21564,21565,21566,21567,21568,21569,21570,21571,21572,21573,21574,21575,21576,21577,21578,21579,21580,21581,21582,21583,21584,21585,21586,21587,21588,21589,21590,21591,21592,21593,21594,21595,21596,21597,21598,21599,21600,21601,21602,21603,21604,21605,21606,21607,21608,21609,21610,21611,21612,21613,21614,21615,21616,21617,21618,21619,21620,21621,21622,21623,21624,21625,21626,21627,21628,21629,21630,21631,21632,21633,21634,21635,21636,21637,21638,21639,21640,21641,21642,21643,21644,21645,21646,21647,21648,21649,21650,21651,21652,21653,21654,21655,21656,21657,21658,21659,21660,21661,21662,21663,21664,21665,21666,21667,21668,21669,21670,21671,21672,21673,21674,21675,21676,21677,21678,21679,21680,21681,21682,21683,21684,21685,21686,21687,21688,21689,21690,21691,21692,21693,21694,21695,21696,21697,21698,21699,21700,21701,21702,21703,21704,21705,21706,21707,21708,21709,21710,21711,21712,21713,21714,21715,21716,21717,21718,21719,21720,21721,21722,21723,21724,21725,21726,21727,21728,21729,21730,21731,21732,21733,21734,21735,21736,21737,21738,21739,21740,21741,21742,21743,21744,21745,21746,21747,21748,21749,21750,21751,21752,21753,21754,21755,21756,21757,21758,21759,21760,21761,21762,21763,21764,21765,21766,21767,21768,21769,21770,21771,21772,21773,21774,21775,21776,21777,21778,21779,21780,21781,21782,21783,21784,21785,21786,21787,21788,21789,21790,21791,21792,21793,21794,21795,21796,21797,21798,21799,21800,21801,21802,21803,21804,21805,21806,21807,21808,21809,21810,21811,21812,21813,21814,21815,21816,21817,21818,21819,21820,21821,21822,21823,21824,21825,21826,21827,21828,21829,21830,21831,21832,21833,21834,21835,21836,21837,21838,21839,21840,21841,21842,21843,21844,21845,21846,21847,21848,21849,21850,21851,21852,21853,21854,21855,21856,21857,21858,21859,21860,21861,21862,21863,21864,21865,21866,21867,21868,21869,21870,21871,21872,21873,21874,21875,21876,21877,21878,21879,21880,21881,21882,21883,21884,21885,21886,21887,21888,21889,21890,21891,21892,21893,21894,21895,21896,21897,21898,21899,21900,21901,21902,21903,21904,21905,21906,21907,21908,21909,21910,21911,21912,21913,21914,21915,21916,21917,21918,21919,21920,21921,21922,21923,21924,21925,21926,21927,21928,21929,21930,21931,21932,21933,21934,21935,21936,21937,21938,21939,21940,21941,21942,21943,21944,21945,21946,21947,21948,21949,21950,21951,21952,21953,21954,21955,21956,21957,21958,21959,21960,21961,21962,21963,21964,21965,21966,21967,21968,21969,21970,21971,21972,21973,21974,21975,21976,21977,21978,21979,21980,21981,21982,21983,21984,21985,21986,21987,21988,21989,21990,21991,21992,21993,21994,21995,21996,21997,21998,21999,22000,22001,22002,22003,22004,22005,22006,22007,22008,22009,22010,22011,22012,22013,22014,22015,22016,22017,22018,22019,22020,22021,22022,22023,22024,22025,22026,22027,22028,22029,22030,22031,22032,22033,22034,22035,22036,22037,22038,22039,22040,22041,22042,22043,22044,22045,22046,22047,22048,22049,22050,22051,22052,22053,22054,22055,22056,22057,22058,22059,22060,22061,22062,22063,22064,22065,22066,22067,22068,22069,22070,22071,22072,22073,22074,22075,22076,22077,22078,22079,22080,22081,22082,22083,22084,22085,22086,22087,22088,22089,22090,22091,22092,22093,22094,22095,22096,22097,22098,22099,22100,22101,22102,22103,22104,22105,22106,22107,22108,22109,22110,22111,22112,22113,22114,22115,22116,22117,22118,22119,22120,22121,22122,22123,22124,22125,22126,22127,22128,22129,22130,22131,22132,22133,22134,22135,22136,22137,22138,22139,22140,22141,22142,22143,22144,22145,22146,22147,22148,22149,22150,22151,22152,22153,22154,22155,22156,22157,22158,22159,22160,22161,22162,22163,22164,22165,22166,22167,22168,22169,22170,22171,22172,22173,22174,22175,22176,22177,22178,22179,22180,22181,22182,22183,22184,22185,22186,22187,22188,22189,22190,22191,22192,22193,22194,22195,22196,22197,22198,22199,22200,22201,22202,22203,22204,22205,22206,22207,22208,22209,22210,22211,22212,22213,22214,22215,22216,22217,22218,22219,22220,22221,22222,22223,22224,22225,22226,22227,22228,22229,22230,22231,22232,22233,22234,22235,22236,22237,22238,22239,22240,22241,22242,22243,22244,22245,22246,22247,22248,22249,22250,22251,22252,22253,22254,22255,22256,22257,22258,22259,22260,22261,22262,22263,22264,22265,22266,22267,22268,22269,22270,22271,22272,22273,22274,22275,22276,22277,22278,22279,22280,22281,22282,22283,22284,22285,22286,22287,22288,22289,22290,22291,22292,22293,22294,22295,22296,22297,22298,22299,22300,22301,22302,22303,22304,22305,22306,22307,22308,22309,22310,22311,22312,22313,22314,22315,22316,22317,22318,22319,22320,22321,22322,22323,22324,22325,22326,22327,22328,22329,22330,22331,22332,22333,22334,22335,22336,22337,22338,22339,22340,22341,22342,22343,22344,22345,22346,22347,22348,22349,22350,22351,22352,22353,22354,22355,22356,22357,22358,22359,22360,22361,22362,22363,22364,22365,22366,22367,22368,22369,22370,22371,22372,22373,22374,22375,22376,22377,22378,22379,22380,22381,22382,22383,22384,22385,22386,22387,22388,22389,22390,22391,22392,22393,22394,22395,22396,22397,22398,22399,22400,22401,22402,22403,22404,22405,22406,22407,22408,22409,22410,22411,22412,22413,22414,22415,22416,22417,22418,22419,22420,22421,22422,22423,22424,22425,22426,22427,22428,22429,22430,22431,22432,22433,22434,22435,22436,22437,22438,22439,22440,22441,22442,22443,22444,22445,22446,22447,22448,22449,22450,22451,22452,22453,22454,22455,22456,22457,22458,22459,22460,22461,22462,22463,22464,22465,22466,22467,22468,22469,22470,22471,22472,22473,22474,22475,22476,22477,22478,22479,22480,22481,22482,22483,22484,22485,22486,22487,22488,22489,22490,22491,22492,22493,22494,22495,22496,22497,22498,22499,22500,22501,22502,22503,22504,22505,22506,22507,22508,22509,22510,22511,22512,22513,22514,22515,22516,22517,22518,22519,22520,22521,22522,22523,22524,22525,22526,22527,22528,22529,22530,22531,22532,22533,22534,22535,22536,22537,22538,22539,22540,22541,22542,22543,22544,22545,22546,22547,22548,22549,22550,22551,22552,22553,22554,22555,22556,22557,22558,22559,22560,22561,22562,22563,22564,22565,22566,22567,22568,22569,22570,22571,22572,22573,22574,22575,22576,22577,22578,22579,22580,22581,22582,22583,22584,22585,22586,22587,22588,22589,22590,22591,22592,22593,22594,22595,22596,22597,22598,22599,22600,22601,22602,22603,22604,22605,22606,22607,22608,22609,22610,22611,22612,22613,22614,22615,22616,22617,22618,22619,22620,22621,22622,22623,22624,22625,22626,22627,22628,22629,22630,22631,22632,22633,22634,22635,22636,22637,22638,22639,22640,22641,22642,22643,22644,22645,22646,22647,22648,22649,22650,22651,22652,22653,22654,22655,22656,22657,22658,22659,22660,22661,22662,22663,22664,22665,22666,22667,22668,22669,22670,22671,22672,22673,22674,22675,22676,22677,22678,22679,22680,22681,22682,22683,22684,22685,22686,22687,22688,22689,22690,22691,22692,22693,22694,22695,22696,22697,22698,22699,22700,22701,22702,22703,22704,22705,22706,22707,22708,22709,22710,22711,22712,22713,22714,22715,22716,22717,22718,22719,22720,22721,22722,22723,22724,22725,22726,22727,22728,22729,22730,22731,22732,22733,22734,22735,22736,22737,22738,22739,22740,22741,22742,22743,22744,22745,22746,22747,22748,22749,22750,22751,22752,22753,22754,22755,22756,22757,22758,22759,22760,22761,22762,22763,22764,22765,22766,22767,22768,22769,22770,22771,22772,22773,22774,22775,22776,22777,22778,22779,22780,22781,22782,22783,22784,22785,22786,22787,22788,22789,22790,22791,22792,22793,22794,22795,22796,22797,22798,22799,22800,22801,22802,22803,22804,22805,22806,22807,22808,22809,22810,22811,22812,22813,22814,22815,22816,22817,22818,22819,22820,22821,22822,22823,22824,22825,22826,22827,22828,22829,22830,22831,22832,22833,22834,22835,22836,22837,22838,22839,22840,22841,22842,22843,22844,22845,22846,22847,22848,22849,22850,22851,22852,22853,22854,22855,22856,22857,22858,22859,22860,22861,22862,22863,22864,22865,22866,22867,22868,22869,22870,22871,22872,22873,22874,22875,22876,22877,22878,22879,22880,22881,22882,22883,22884,22885,22886,22887,22888,22889,22890,22891,22892,22893,22894,22895,22896,22897,22898,22899,22900,22901,22902,22903,22904,22905,22906,22907,22908,22909,22910,22911,22912,22913,22914,22915,22916,22917,22918,22919,22920,22921,22922,22923,22924,22925,22926,22927,22928,22929,22930,22931,22932,22933,22934,22935,22936,22937,22938,22939,22940,22941,22942,22943,22944,22945,22946,22947,22948,22949,22950,22951,22952,22953,22954,22955,22956,22957,22958,22959,22960,22961,22962,22963,22964,22965,22966,22967,22968,22969,22970,22971,22972,22973,22974,22975,22976,22977,22978,22979,22980,22981,22982,22983,22984,22985,22986,22987,22988,22989,22990,22991,22992,22993,22994,22995,22996,22997,22998,22999,23000,23001,23002,23003,23004,23005,23006,23007,23008,23009,23010,23011,23012,23013,23014,23015,23016,23017,23018,23019,23020,23021,23022,23023,23024,23025,23026,23027,23028,23029,23030,23031,23032,23033,23034,23035,23036,23037,23038,23039,23040,23041,23042,23043,23044,23045,23046,23047,23048,23049,23050,23051,23052,23053,23054,23055,23056,23057,23058,23059,23060,23061,23062,23063,23064,23065,23066,23067,23068,23069,23070,23071,23072,23073,23074,23075,23076,23077,23078,23079,23080,23081,23082,23083,23084,23085,23086,23087,23088,23089,23090,23091,23092,23093,23094,23095,23096,23097,23098,23099,23100,23101,23102,23103,23104,23105,23106,23107,23108,23109,23110,23111,23112,23113,23114,23115,23116,23117,23118,23119,23120,23121,23122,23123,23124,23125,23126,23127,23128,23129,23130,23131,23132,23133,23134,23135,23136,23137,23138,23139,23140,23141,23142,23143,23144,23145,23146,23147,23148,23149,23150,23151,23152,23153,23154,23155,23156,23157,23158,23159,23160,23161,23162,23163,23164,23165,23166,23167,23168,23169,23170,23171,23172,23173,23174,23175,23176,23177,23178,23179,23180,23181,23182,23183,23184,23185,23186,23187,23188,23189,23190,23191,23192,23193,23194,23195,23196,23197,23198,23199,23200,23201,23202,23203,23204,23205,23206,23207,23208,23209,23210,23211,23212,23213,23214,23215,23216,23217,23218,23219,23220,23221,23222,23223,23224,23225,23226,23227,23228,23229,23230,23231,23232,23233,23234,23235,23236,23237,23238,23239,23240,23241,23242,23243,23244,23245,23246,23247,23248,23249,23250,23251,23252,23253,23254,23255,23256,23257,23258,23259,23260,23261,23262,23263,23264,23265,23266,23267,23268,23269,23270,23271,23272,23273,23274,23275,23276,23277,23278,23279,23280,23281,23282,23283,23284,23285,23286,23287,23288,23289,23290,23291,23292,23293,23294,23295,23296,23297,23298,23299,23300,23301,23302,23303,23304,23305,23306,23307,23308,23309,23310,23311,23312,23313,23314,23315,23316,23317,23318,23319,23320,23321,23322,23323,23324,23325,23326,23327,23328,23329,23330,23331,23332,23333,23334,23335,23336,23337,23338,23339,23340,23341,23342,23343,23344,23345,23346,23347,23348,23349,23350,23351,23352,23353,23354,23355,23356,23357,23358,23359,23360,23361,23362,23363,23364,23365,23366,23367,23368,23369,23370,23371,23372,23373,23374,23375,23376,23377,23378,23379,23380,23381,23382,23383,23384,23385,23386,23387,23388,23389,23390,23391,23392,23393,23394,23395,23396,23397,23398,23399,23400,23401,23402,23403,23404,23405,23406,23407,23408,23409,23410,23411,23412,23413,23414,23415,23416,23417,23418,23419,23420,23421,23422,23423,23424,23425,23426,23427,23428,23429,23430,23431,23432,23433,23434,23435,23436,23437,23438,23439,23440,23441,23442,23443,23444,23445,23446,23447,23448,23449,23450,23451,23452,23453,23454,23455,23456,23457,23458,23459,23460,23461,23462,23463,23464,23465,23466,23467,23468,23469,23470,23471,23472,23473,23474,23475,23476,23477,23478,23479,23480,23481,23482,23483,23484,23485,23486,23487,23488,23489,23490,23491,23492,23493,23494,23495,23496,23497,23498,23499,23500,23501,23502,23503,23504,23505,23506,23507,23508,23509,23510,23511,23512,23513,23514,23515,23516,23517,23518,23519,23520,23521,23522,23523,23524,23525,23526,23527,23528,23529,23530,23531,23532,23533,23534,23535,23536,23537,23538,23539,23540,23541,23542,23543,23544,23545,23546,23547,23548,23549,23550,23551,23552,23553,23554,23555,23556,23557,23558,23559,23560,23561,23562,23563,23564,23565,23566,23567,23568,23569,23570,23571,23572,23573,23574,23575,23576,23577,23578,23579,23580,23581,23582,23583,23584,23585,23586,23587,23588,23589,23590,23591,23592,23593,23594,23595,23596,23597,23598,23599,23600,23601,23602,23603,23604,23605,23606,23607,23608,23609,23610,23611,23612,23613,23614,23615,23616,23617,23618,23619,23620,23621,23622,23623,23624,23625,23626,23627,23628,23629,23630,23631,23632,23633,23634,23635,23636,23637,23638,23639,23640,23641,23642,23643,23644,23645,23646,23647,23648,23649,23650,23651,23652,23653,23654,23655,23656,23657,23658,23659,23660,23661,23662,23663,23664,23665,23666,23667,23668,23669,23670,23671,23672,23673,23674,23675,23676,23677,23678,23679,23680,23681,23682,23683,23684,23685,23686,23687,23688,23689,23690,23691,23692,23693,23694,23695,23696,23697,23698,23699,23700,23701,23702,23703,23704,23705,23706,23707,23708,23709,23710,23711,23712,23713,23714,23715,23716,23717,23718,23719,23720,23721,23722,23723,23724,23725,23726,23727,23728,23729,23730,23731,23732,23733,23734,23735,23736,23737,23738,23739,23740,23741,23742,23743,23744,23745,23746,23747,23748,23749,23750,23751,23752,23753,23754,23755,23756,23757,23758,23759,23760,23761,23762,23763,23764,23765,23766,23767,23768,23769,23770,23771,23772,23773,23774,23775,23776,23777,23778,23779,23780,23781,23782,23783,23784,23785,23786,23787,23788,23789,23790,23791,23792,23793,23794,23795,23796,23797,23798,23799,23800,23801,23802,23803,23804,23805,23806,23807,23808,23809,23810,23811,23812,23813,23814,23815,23816,23817,23818,23819,23820,23821,23822,23823,23824,23825,23826,23827,23828,23829,23830,23831,23832,23833,23834,23835,23836,23837,23838,23839,23840,23841,23842,23843,23844,23845,23846,23847,23848,23849,23850,23851,23852,23853,23854,23855,23856,23857,23858,23859,23860,23861,23862,23863,23864,23865,23866,23867,23868,23869,23870,23871,23872,23873,23874,23875,23876,23877,23878,23879,23880,23881,23882,23883,23884,23885,23886,23887,23888,23889,23890,23891,23892,23893,23894,23895,23896,23897,23898,23899,23900,23901,23902,23903,23904,23905,23906,23907,23908,23909,23910,23911,23912,23913,23914,23915,23916,23917,23918,23919,23920,23921,23922,23923,23924,23925,23926,23927,23928,23929,23930,23931,23932,23933,23934,23935,23936,23937,23938,23939,23940,23941,23942,23943,23944,23945,23946,23947,23948,23949,23950,23951,23952,23953,23954,23955,23956,23957,23958,23959,23960,23961,23962,23963,23964,23965,23966,23967,23968,23969,23970,23971,23972,23973,23974,23975,23976,23977,23978,23979,23980,23981,23982,23983,23984,23985,23986,23987,23988,23989,23990,23991,23992,23993,23994,23995,23996,23997,23998,23999,24000,24001,24002,24003,24004,24005,24006,24007,24008,24009,24010,24011,24012,24013,24014,24015,24016,24017,24018,24019,24020,24021,24022,24023,24024,24025,24026,24027,24028,24029,24030,24031,24032,24033,24034,24035,24036,24037,24038,24039,24040,24041,24042,24043,24044,24045,24046,24047,24048,24049,24050,24051,24052,24053,24054,24055,24056,24057,24058,24059,24060,24061,24062,24063,24064,24065,24066,24067,24068,24069,24070,24071,24072,24073,24074,24075,24076,24077,24078,24079,24080,24081,24082,24083,24084,24085,24086,24087,24088,24089,24090,24091,24092,24093,24094,24095,24096,24097,24098,24099,24100,24101,24102,24103,24104,24105,24106,24107,24108,24109,24110,24111,24112,24113,24114,24115,24116,24117,24118,24119,24120,24121,24122,24123,24124,24125,24126,24127,24128,24129,24130,24131,24132,24133,24134,24135,24136,24137,24138,24139,24140,24141,24142,24143,24144,24145,24146,24147,24148,24149,24150,24151,24152,24153,24154,24155,24156,24157,24158,24159,24160,24161,24162,24163,24164,24165,24166,24167,24168,24169,24170,24171,24172,24173,24174,24175,24176,24177,24178,24179,24180,24181,24182,24183,24184,24185,24186,24187,24188,24189,24190,24191,24192,24193,24194,24195,24196,24197,24198,24199,24200,24201,24202,24203,24204,24205,24206,24207,24208,24209,24210,24211,24212,24213,24214,24215,24216,24217,24218,24219,24220,24221,24222,24223,24224,24225,24226,24227,24228,24229,24230,24231,24232,24233,24234,24235,24236,24237,24238,24239,24240,24241,24242,24243,24244,24245,24246,24247,24248,24249,24250,24251,24252,24253,24254,24255,24256,24257,24258,24259,24260,24261,24262,24263,24264,24265,24266,24267,24268,24269,24270,24271,24272,24273,24274,24275,24276,24277,24278,24279,24280,24281,24282,24283,24284,24285,24286,24287,24288,24289,24290,24291,24292,24293,24294,24295,24296,24297,24298,24299,24300,24301,24302,24303,24304,24305,24306,24307,24308,24309,24310,24311,24312,24313,24314,24315,24316,24317,24318,24319,24320,24321,24322,24323,24324,24325,24326,24327,24328,24329,24330,24331,24332,24333,24334,24335,24336,24337,24338,24339,24340,24341,24342,24343,24344,24345,24346,24347,24348,24349,24350,24351,24352,24353,24354,24355,24356,24357,24358,24359,24360,24361,24362,24363,24364,24365,24366,24367,24368,24369,24370,24371,24372,24373,24374,24375,24376,24377,24378,24379,24380,24381,24382,24383,24384,24385,24386,24387,24388,24389,24390,24391,24392,24393,24394,24395,24396,24397,24398,24399,24400,24401,24402,24403,24404,24405,24406,24407,24408,24409,24410,24411,24412,24413,24414,24415,24416,24417,24418,24419,24420,24421,24422,24423,24424,24425,24426,24427,24428,24429,24430,24431,24432,24433,24434,24435,24436,24437,24438,24439,24440,24441,24442,24443,24444,24445,24446,24447,24448,24449,24450,24451,24452,24453,24454,24455,24456,24457,24458,24459,24460,24461,24462,24463,24464,24465,24466,24467,24468,24469,24470,24471,24472,24473,24474,24475,24476,24477,24478,24479,24480,24481,24482,24483,24484,24485,24486,24487,24488,24489,24490,24491,24492,24493,24494,24495,24496,24497,24498,24499,24500,24501,24502,24503,24504,24505,24506,24507,24508,24509,24510,24511,24512,24513,24514,24515,24516,24517,24518,24519,24520,24521,24522,24523,24524,24525,24526,24527,24528,24529,24530,24531,24532,24533,24534,24535,24536,24537,24538,24539,24540,24541,24542,24543,24544,24545,24546,24547,24548,24549,24550,24551,24552,24553,24554,24555,24556,24557,24558,24559,24560,24561,24562,24563,24564,24565,24566,24567,24568,24569,24570,24571,24572,24573,24574,24575,24576,24577,24578,24579,24580,24581,24582,24583,24584,24585,24586,24587,24588,24589,24590,24591,24592,24593,24594,24595,24596,24597,24598,24599,24600,24601,24602,24603,24604,24605,24606,24607,24608,24609,24610,24611,24612,24613,24614,24615,24616,24617,24618,24619,24620,24621,24622,24623,24624,24625,24626,24627,24628,24629,24630,24631,24632,24633,24634,24635,24636,24637,24638,24639,24640,24641,24642,24643,24644,24645,24646,24647,24648,24649,24650,24651,24652,24653,24654,24655,24656,24657,24658,24659,24660,24661,24662,24663,24664,24665,24666,24667,24668,24669,24670,24671,24672,24673,24674,24675,24676,24677,24678,24679,24680,24681,24682,24683,24684,24685,24686,24687,24688,24689,24690,24691,24692,24693,24694,24695,24696,24697,24698,24699,24700,24701,24702,24703,24704,24705,24706,24707,24708,24709,24710,24711,24712,24713,24714,24715,24716,24717,24718,24719,24720,24721,24722,24723,24724,24725,24726,24727,24728,24729,24730,24731,24732,24733,24734,24735,24736,24737,24738,24739,24740,24741,24742,24743,24744,24745,24746,24747,24748,24749,24750,24751,24752,24753,24754,24755,24756,24757,24758,24759,24760,24761,24762,24763,24764,24765,24766,24767,24768,24769,24770,24771,24772,24773,24774,24775,24776,24777,24778,24779,24780,24781,24782,24783,24784,24785,24786,24787,24788,24789,24790,24791,24792,24793,24794,24795,24796,24797,24798,24799,24800,24801,24802,24803,24804,24805,24806,24807,24808,24809,24810,24811,24812,24813,24814,24815,24816,24817,24818,24819,24820,24821,24822,24823,24824,24825,24826,24827,24828,24829,24830,24831,24832,24833,24834,24835,24836,24837,24838,24839,24840,24841,24842,24843,24844,24845,24846,24847,24848,24849,24850,24851,24852,24853,24854,24855,24856,24857,24858,24859,24860,24861,24862,24863,24864,24865,24866,24867,24868,24869,24870,24871,24872,24873,24874,24875,24876,24877,24878,24879,24880,24881,24882,24883,24884,24885,24886,24887,24888,24889,24890,24891,24892,24893,24894,24895,24896,24897,24898,24899,24900,24901,24902,24903,24904,24905,24906,24907,24908,24909,24910,24911,24912,24913,24914,24915,24916,24917,24918,24919,24920,24921,24922,24923,24924,24925,24926,24927,24928,24929,24930,24931,24932,24933,24934,24935,24936,24937,24938,24939,24940,24941,24942,24943,24944,24945,24946,24947,24948,24949,24950,24951,24952,24953,24954,24955,24956,24957,24958,24959,24960,24961,24962,24963,24964,24965,24966,24967,24968,24969,24970,24971,24972,24973,24974,24975,24976,24977,24978,24979,24980,24981,24982,24983,24984,24985,24986,24987,24988,24989,24990,24991,24992,24993,24994,24995,24996,24997,24998,24999,25000,25001,25002,25003,25004,25005,25006,25007,25008,25009,25010,25011,25012,25013,25014,25015,25016,25017,25018,25019,25020,25021,25022,25023,25024,25025,25026,25027,25028,25029,25030,25031,25032,25033,25034,25035,25036,25037,25038,25039,25040,25041,25042,25043,25044,25045,25046,25047,25048,25049,25050,25051,25052,25053,25054,25055,25056,25057,25058,25059,25060,25061,25062,25063,25064,25065,25066,25067,25068,25069,25070,25071,25072,25073,25074,25075,25076,25077,25078,25079,25080,25081,25082,25083,25084,25085,25086,25087,25088,25089,25090,25091,25092,25093,25094,25095,25096,25097,25098,25099,25100,25101,25102,25103,25104,25105,25106,25107,25108,25109,25110,25111,25112,25113,25114,25115,25116,25117,25118,25119,25120,25121,25122,25123,25124,25125,25126,25127,25128,25129,25130,25131,25132,25133,25134,25135,25136,25137,25138,25139,25140,25141,25142,25143,25144,25145,25146,25147,25148,25149,25150,25151,25152,25153,25154,25155,25156,25157,25158,25159,25160,25161,25162,25163,25164,25165,25166,25167,25168,25169,25170,25171,25172,25173,25174,25175,25176,25177,25178,25179,25180,25181,25182,25183,25184,25185,25186,25187,25188,25189,25190,25191,25192,25193,25194,25195,25196,25197,25198,25199,25200,25201,25202,25203,25204,25205,25206,25207,25208,25209,25210,25211,25212,25213,25214,25215,25216,25217,25218,25219,25220,25221,25222,25223,25224,25225,25226,25227,25228,25229,25230,25231,25232,25233,25234,25235,25236,25237,25238,25239,25240,25241,25242,25243,25244,25245,25246,25247,25248,25249,25250,25251,25252,25253,25254,25255,25256,25257,25258,25259,25260,25261,25262,25263,25264,25265,25266,25267,25268,25269,25270,25271,25272,25273,25274,25275,25276,25277,25278,25279,25280,25281,25282,25283,25284,25285,25286,25287,25288,25289,25290,25291,25292,25293,25294,25295,25296,25297,25298,25299,25300,25301,25302,25303,25304,25305,25306,25307,25308,25309,25310,25311,25312,25313,25314,25315,25316,25317,25318,25319,25320,25321,25322,25323,25324,25325,25326,25327,25328,25329,25330,25331,25332,25333,25334,25335,25336,25337,25338,25339,25340,25341,25342,25343,25344,25345,25346,25347,25348,25349,25350,25351,25352,25353,25354,25355,25356,25357,25358,25359,25360,25361,25362,25363,25364,25365,25366,25367,25368,25369,25370,25371,25372,25373,25374,25375,25376,25377,25378,25379,25380,25381,25382,25383,25384,25385,25386,25387,25388,25389,25390,25391,25392,25393,25394,25395,25396,25397,25398,25399,25400,25401,25402,25403,25404,25405,25406,25407,25408,25409,25410,25411,25412,25413,25414,25415,25416,25417,25418,25419,25420,25421,25422,25423,25424,25425,25426,25427,25428,25429,25430,25431,25432,25433,25434,25435,25436,25437,25438,25439,25440,25441,25442,25443,25444,25445,25446,25447,25448,25449,25450,25451,25452,25453,25454,25455,25456,25457,25458,25459,25460,25461,25462,25463,25464,25465,25466,25467,25468,25469,25470,25471,25472,25473,25474,25475,25476,25477,25478,25479,25480,25481,25482,25483,25484,25485,25486,25487,25488,25489,25490,25491,25492,25493,25494,25495,25496,25497,25498,25499,25500,25501,25502,25503,25504,25505,25506,25507,25508,25509,25510,25511,25512,25513,25514,25515,25516,25517,25518,25519,25520,25521,25522,25523,25524,25525,25526,25527,25528,25529,25530,25531,25532,25533,25534,25535,25536,25537,25538,25539,25540,25541,25542,25543,25544,25545,25546,25547,25548,25549,25550,25551,25552,25553,25554,25555,25556,25557,25558,25559,25560,25561,25562,25563,25564,25565,25566,25567,25568,25569,25570,25571,25572,25573,25574,25575,25576,25577,25578,25579,25580,25581,25582,25583,25584,25585,25586,25587,25588,25589,25590,25591,25592,25593,25594,25595,25596,25597,25598,25599,25600,25601,25602,25603,25604,25605,25606,25607,25608,25609,25610,25611,25612,25613,25614,25615,25616,25617,25618,25619,25620,25621,25622,25623,25624,25625,25626,25627,25628,25629,25630,25631,25632,25633,25634,25635,25636,25637,25638,25639,25640,25641,25642,25643,25644,25645,25646,25647,25648,25649,25650,25651,25652,25653,25654,25655,25656,25657,25658,25659,25660,25661,25662,25663,25664,25665,25666,25667,25668,25669,25670,25671,25672,25673,25674,25675,25676,25677,25678,25679,25680,25681,25682,25683,25684,25685,25686,25687,25688,25689,25690,25691,25692,25693,25694,25695,25696,25697,25698,25699,25700,25701,25702,25703,25704,25705,25706,25707,25708,25709,25710,25711,25712,25713,25714,25715,25716,25717,25718,25719,25720,25721,25722,25723,25724,25725,25726,25727,25728,25729,25730,25731,25732,25733,25734,25735,25736,25737,25738,25739,25740,25741,25742,25743,25744,25745,25746,25747,25748,25749,25750,25751,25752,25753,25754,25755,25756,25757,25758,25759,25760,25761,25762,25763,25764,25765,25766,25767,25768,25769,25770,25771,25772,25773,25774,25775,25776,25777,25778,25779,25780,25781,25782,25783,25784,25785,25786,25787,25788,25789,25790,25791,25792,25793,25794,25795,25796,25797,25798,25799,25800,25801,25802,25803,25804,25805,25806,25807,25808,25809,25810,25811,25812,25813,25814,25815,25816,25817,25818,25819,25820,25821,25822,25823,25824,25825,25826,25827,25828,25829,25830,25831,25832,25833,25834,25835,25836,25837,25838,25839,25840,25841,25842,25843,25844,25845,25846,25847,25848,25849,25850,25851,25852,25853,25854,25855,25856,25857,25858,25859,25860,25861,25862,25863,25864,25865,25866,25867,25868,25869,25870,25871,25872,25873,25874,25875,25876,25877,25878,25879,25880,25881,25882,25883,25884,25885,25886,25887,25888,25889,25890,25891,25892,25893,25894,25895,25896,25897,25898,25899,25900,25901,25902,25903,25904,25905,25906,25907,25908,25909,25910,25911,25912,25913,25914,25915,25916,25917,25918,25919,25920,25921,25922,25923,25924,25925,25926,25927,25928,25929,25930,25931,25932,25933,25934,25935,25936,25937,25938,25939,25940,25941,25942,25943,25944,25945,25946,25947,25948,25949,25950,25951,25952,25953,25954,25955,25956,25957,25958,25959,25960,25961,25962,25963,25964,25965,25966,25967,25968,25969,25970,25971,25972,25973,25974,25975,25976,25977,25978,25979,25980,25981,25982,25983,25984,25985,25986,25987,25988,25989,25990,25991,25992,25993,25994,25995,25996,25997,25998,25999,26000,26001,26002,26003,26004,26005,26006,26007,26008,26009,26010,26011,26012,26013,26014,26015,26016,26017,26018,26019,26020,26021,26022,26023,26024,26025,26026,26027,26028,26029,26030,26031,26032,26033,26034,26035,26036,26037,26038,26039,26040,26041,26042,26043,26044,26045,26046,26047,26048,26049,26050,26051,26052,26053,26054,26055,26056,26057,26058,26059,26060,26061,26062,26063,26064,26065,26066,26067,26068,26069,26070,26071,26072,26073,26074,26075,26076,26077,26078,26079,26080,26081,26082,26083,26084,26085,26086,26087,26088,26089,26090,26091,26092,26093,26094,26095,26096,26097,26098,26099,26100,26101,26102,26103,26104,26105,26106,26107,26108,26109,26110,26111,26112,26113,26114,26115,26116,26117,26118,26119,26120,26121,26122,26123,26124,26125,26126,26127,26128,26129,26130,26131,26132,26133,26134,26135,26136,26137,26138,26139,26140,26141,26142,26143,26144,26145,26146,26147,26148,26149,26150,26151,26152,26153,26154,26155,26156,26157,26158,26159,26160,26161,26162,26163,26164,26165,26166,26167,26168,26169,26170,26171,26172,26173,26174,26175,26176,26177,26178,26179,26180,26181,26182,26183,26184,26185,26186,26187,26188,26189,26190,26191,26192,26193,26194,26195,26196,26197,26198,26199,26200,26201,26202,26203,26204,26205,26206,26207,26208,26209,26210,26211,26212,26213,26214,26215,26216,26217,26218,26219,26220,26221,26222,26223,26224,26225,26226,26227,26228,26229,26230,26231,26232,26233,26234,26235,26236,26237,26238,26239,26240,26241,26242,26243,26244,26245,26246,26247,26248,26249,26250,26251,26252,26253,26254,26255,26256,26257,26258,26259,26260,26261,26262,26263,26264,26265,26266,26267,26268,26269,26270,26271,26272,26273,26274,26275,26276,26277,26278,26279,26280,26281,26282,26283,26284,26285,26286,26287,26288,26289,26290,26291,26292,26293,26294,26295,26296,26297,26298,26299,26300,26301,26302,26303,26304,26305,26306,26307,26308,26309,26310,26311,26312,26313,26314,26315,26316,26317,26318,26319,26320,26321,26322,26323,26324,26325,26326,26327,26328,26329,26330,26331,26332,26333,26334,26335,26336,26337,26338,26339,26340,26341,26342,26343,26344,26345,26346,26347,26348,26349,26350,26351,26352,26353,26354,26355,26356,26357,26358,26359,26360,26361,26362,26363,26364,26365,26366,26367,26368,26369,26370,26371,26372,26373,26374,26375,26376,26377,26378,26379,26380,26381,26382,26383,26384,26385,26386,26387,26388,26389,26390,26391,26392,26393,26394,26395,26396,26397,26398,26399,26400,26401,26402,26403,26404,26405,26406,26407,26408,26409,26410,26411,26412,26413,26414,26415,26416,26417,26418,26419,26420,26421,26422,26423,26424,26425,26426,26427,26428,26429,26430,26431,26432,26433,26434,26435,26436,26437,26438,26439,26440,26441,26442,26443,26444,26445,26446,26447,26448,26449,26450,26451,26452,26453,26454,26455,26456,26457,26458,26459,26460,26461,26462,26463,26464,26465,26466,26467,26468,26469,26470,26471,26472,26473,26474,26475,26476,26477,26478,26479,26480,26481,26482,26483,26484,26485,26486,26487,26488,26489,26490,26491,26492,26493,26494,26495,26496,26497,26498,26499,26500,26501,26502,26503,26504,26505,26506,26507,26508,26509,26510,26511,26512,26513,26514,26515,26516,26517,26518,26519,26520,26521,26522,26523,26524,26525,26526,26527,26528,26529,26530,26531,26532,26533,26534,26535,26536,26537,26538,26539,26540,26541,26542,26543,26544,26545,26546,26547,26548,26549,26550,26551,26552,26553,26554,26555,26556,26557,26558,26559,26560,26561,26562,26563,26564,26565,26566,26567,26568,26569,26570,26571,26572,26573,26574,26575,26576,26577,26578,26579,26580,26581,26582,26583,26584,26585,26586,26587,26588,26589,26590,26591,26592,26593,26594,26595,26596,26597,26598,26599,26600,26601,26602,26603,26604,26605,26606,26607,26608,26609,26610,26611,26612,26613,26614,26615,26616,26617,26618,26619,26620,26621,26622,26623,26624,26625,26626,26627,26628,26629,26630,26631,26632,26633,26634,26635,26636,26637,26638,26639,26640,26641,26642,26643,26644,26645,26646,26647,26648,26649,26650,26651,26652,26653,26654,26655,26656,26657,26658,26659,26660,26661,26662,26663,26664,26665,26666,26667,26668,26669,26670,26671,26672,26673,26674,26675,26676,26677,26678,26679,26680,26681,26682,26683,26684,26685,26686,26687,26688,26689,26690,26691,26692,26693,26694,26695,26696,26697,26698,26699,26700,26701,26702,26703,26704,26705,26706,26707,26708,26709,26710,26711,26712,26713,26714,26715,26716,26717,26718,26719,26720,26721,26722,26723,26724,26725,26726,26727,26728,26729,26730,26731,26732,26733,26734,26735,26736,26737,26738,26739,26740,26741,26742,26743,26744,26745,26746,26747,26748,26749,26750,26751,26752,26753,26754,26755,26756,26757,26758,26759,26760,26761,26762,26763,26764,26765,26766,26767,26768,26769,26770,26771,26772,26773,26774,26775,26776,26777,26778,26779,26780,26781,26782,26783,26784,26785,26786,26787,26788,26789,26790,26791,26792,26793,26794,26795,26796,26797,26798,26799,26800,26801,26802,26803,26804,26805,26806,26807,26808,26809,26810,26811,26812,26813,26814,26815,26816,26817,26818,26819,26820,26821,26822,26823,26824,26825,26826,26827,26828,26829,26830,26831,26832,26833,26834,26835,26836,26837,26838,26839,26840,26841,26842,26843,26844,26845,26846,26847,26848,26849,26850,26851,26852,26853,26854,26855,26856,26857,26858,26859,26860,26861,26862,26863,26864,26865,26866,26867,26868,26869,26870,26871,26872,26873,26874,26875,26876,26877,26878,26879,26880,26881,26882,26883,26884,26885,26886,26887,26888,26889,26890,26891,26892,26893,26894,26895,26896,26897,26898,26899,26900,26901,26902,26903,26904,26905,26906,26907,26908,26909,26910,26911,26912,26913,26914,26915,26916,26917,26918,26919,26920,26921,26922,26923,26924,26925,26926,26927,26928,26929,26930,26931,26932,26933,26934,26935,26936,26937,26938,26939,26940,26941,26942,26943,26944,26945,26946,26947,26948,26949,26950,26951,26952,26953,26954,26955,26956,26957,26958,26959,26960,26961,26962,26963,26964,26965,26966,26967,26968,26969,26970,26971,26972,26973,26974,26975,26976,26977,26978,26979,26980,26981,26982,26983,26984,26985,26986,26987,26988,26989,26990,26991,26992,26993,26994,26995,26996,26997,26998,26999,27000,27001,27002,27003,27004,27005,27006,27007,27008,27009,27010,27011,27012,27013,27014,27015,27016,27017,27018,27019,27020,27021,27022,27023,27024,27025,27026,27027,27028,27029,27030,27031,27032,27033,27034,27035,27036,27037,27038,27039,27040,27041,27042,27043,27044,27045,27046,27047,27048,27049,27050,27051,27052,27053,27054,27055,27056,27057,27058,27059,27060,27061,27062,27063,27064,27065,27066,27067,27068,27069,27070,27071,27072,27073,27074,27075,27076,27077,27078,27079,27080,27081,27082,27083,27084,27085,27086,27087,27088,27089,27090,27091,27092,27093,27094,27095,27096,27097,27098,27099,27100,27101,27102,27103,27104,27105,27106,27107,27108,27109,27110,27111,27112,27113,27114,27115,27116,27117,27118,27119,27120,27121,27122,27123,27124,27125,27126,27127,27128,27129,27130,27131,27132,27133,27134,27135,27136,27137,27138,27139,27140,27141,27142,27143,27144,27145,27146,27147,27148,27149,27150,27151,27152,27153,27154,27155,27156,27157,27158,27159,27160,27161,27162,27163,27164,27165,27166,27167,27168,27169,27170,27171,27172,27173,27174,27175,27176,27177,27178,27179,27180,27181,27182,27183,27184,27185,27186,27187,27188,27189,27190,27191,27192,27193,27194,27195,27196,27197,27198,27199,27200,27201,27202,27203,27204,27205,27206,27207,27208,27209,27210,27211,27212,27213,27214,27215,27216,27217,27218,27219,27220,27221,27222,27223,27224,27225,27226,27227,27228,27229,27230,27231,27232,27233,27234,27235,27236,27237,27238,27239,27240,27241,27242,27243,27244,27245,27246,27247,27248,27249,27250,27251,27252,27253,27254,27255,27256,27257,27258,27259,27260,27261,27262,27263,27264,27265,27266,27267,27268,27269,27270,27271,27272,27273,27274,27275,27276,27277,27278,27279,27280,27281,27282,27283,27284,27285,27286,27287,27288,27289,27290,27291,27292,27293,27294,27295,27296,27297,27298,27299,27300,27301,27302,27303,27304,27305,27306,27307,27308,27309,27310,27311,27312,27313,27314,27315,27316,27317,27318,27319,27320,27321,27322,27323,27324,27325,27326,27327,27328,27329,27330,27331,27332,27333,27334,27335,27336,27337,27338,27339,27340,27341,27342,27343,27344,27345,27346,27347,27348,27349,27350,27351,27352,27353,27354,27355,27356,27357,27358,27359,27360,27361,27362,27363,27364,27365,27366,27367,27368,27369,27370,27371,27372,27373,27374,27375,27376,27377,27378,27379,27380,27381,27382,27383,27384,27385,27386,27387,27388,27389,27390,27391,27392,27393,27394,27395,27396,27397,27398,27399,27400,27401,27402,27403,27404,27405,27406,27407,27408,27409,27410,27411,27412,27413,27414,27415,27416,27417,27418,27419,27420,27421,27422,27423,27424,27425,27426,27427,27428,27429,27430,27431,27432,27433,27434,27435,27436,27437,27438,27439,27440,27441,27442,27443,27444,27445,27446,27447,27448,27449,27450,27451,27452,27453,27454,27455,27456,27457,27458,27459,27460,27461,27462,27463,27464,27465,27466,27467,27468,27469,27470,27471,27472,27473,27474,27475,27476,27477,27478,27479,27480,27481,27482,27483,27484,27485,27486,27487,27488,27489,27490,27491,27492,27493,27494,27495,27496,27497,27498,27499,27500,27501,27502,27503,27504,27505,27506,27507,27508,27509,27510,27511,27512,27513,27514,27515,27516,27517,27518,27519,27520,27521,27522,27523,27524,27525,27526,27527,27528,27529,27530,27531,27532,27533,27534,27535,27536,27537,27538,27539,27540,27541,27542,27543,27544,27545,27546,27547,27548,27549,27550,27551,27552,27553,27554,27555,27556,27557,27558,27559,27560,27561,27562,27563,27564,27565,27566,27567,27568,27569,27570,27571,27572,27573,27574,27575,27576,27577,27578,27579,27580,27581,27582,27583,27584,27585,27586,27587,27588,27589,27590,27591,27592,27593,27594,27595,27596,27597,27598,27599,27600,27601,27602,27603,27604,27605,27606,27607,27608,27609,27610,27611,27612,27613,27614,27615,27616,27617,27618,27619,27620,27621,27622,27623,27624,27625,27626,27627,27628,27629,27630,27631,27632,27633,27634,27635,27636,27637,27638,27639,27640,27641,27642,27643,27644,27645,27646,27647,27648,27649,27650,27651,27652,27653,27654,27655,27656,27657,27658,27659,27660,27661,27662,27663,27664,27665,27666,27667,27668,27669,27670,27671,27672,27673,27674,27675,27676,27677,27678,27679,27680,27681,27682,27683,27684,27685,27686,27687,27688,27689,27690,27691,27692,27693,27694,27695,27696,27697,27698,27699,27700,27701,27702,27703,27704,27705,27706,27707,27708,27709,27710,27711,27712,27713,27714,27715,27716,27717,27718,27719,27720,27721,27722,27723,27724,27725,27726,27727,27728,27729,27730,27731,27732,27733,27734,27735,27736,27737,27738,27739,27740,27741,27742,27743,27744,27745,27746,27747,27748,27749,27750,27751,27752,27753,27754,27755,27756,27757,27758,27759,27760,27761,27762,27763,27764,27765,27766,27767,27768,27769,27770,27771,27772,27773,27774,27775,27776,27777,27778,27779,27780,27781,27782,27783,27784,27785,27786,27787,27788,27789,27790,27791,27792,27793,27794,27795,27796,27797,27798,27799,27800,27801,27802,27803,27804,27805,27806,27807,27808,27809,27810,27811,27812,27813,27814,27815,27816,27817,27818,27819,27820,27821,27822,27823,27824,27825,27826,27827,27828,27829,27830,27831,27832,27833,27834,27835,27836,27837,27838,27839,27840,27841,27842,27843,27844,27845,27846,27847,27848,27849,27850,27851,27852,27853,27854,27855,27856,27857,27858,27859,27860,27861,27862,27863,27864,27865,27866,27867,27868,27869,27870,27871,27872,27873,27874,27875,27876,27877,27878,27879,27880,27881,27882,27883,27884,27885,27886,27887,27888,27889,27890,27891,27892,27893,27894,27895,27896,27897,27898,27899,27900,27901,27902,27903,27904,27905,27906,27907,27908,27909,27910,27911,27912,27913,27914,27915,27916,27917,27918,27919,27920,27921,27922,27923,27924,27925,27926,27927,27928,27929,27930,27931,27932,27933,27934,27935,27936,27937,27938,27939,27940,27941,27942,27943,27944,27945,27946,27947,27948,27949,27950,27951,27952,27953,27954,27955,27956,27957,27958,27959,27960,27961,27962,27963,27964,27965,27966,27967,27968,27969,27970,27971,27972,27973,27974,27975,27976,27977,27978,27979,27980,27981,27982,27983,27984,27985,27986,27987,27988,27989,27990,27991,27992,27993,27994,27995,27996,27997,27998,27999,28000,28001,28002,28003,28004,28005,28006,28007,28008,28009,28010,28011,28012,28013,28014,28015,28016,28017,28018,28019,28020,28021,28022,28023,28024,28025,28026,28027,28028,28029,28030,28031,28032,28033,28034,28035,28036,28037,28038,28039,28040,28041,28042,28043,28044,28045,28046,28047,28048,28049,28050,28051,28052,28053,28054,28055,28056,28057,28058,28059,28060,28061,28062,28063,28064,28065,28066,28067,28068,28069,28070,28071,28072,28073,28074,28075,28076,28077,28078,28079,28080,28081,28082,28083,28084,28085,28086,28087,28088,28089,28090,28091,28092,28093,28094,28095,28096,28097,28098,28099,28100,28101,28102,28103,28104,28105,28106,28107,28108,28109,28110,28111,28112,28113,28114,28115,28116,28117,28118,28119,28120,28121,28122,28123,28124,28125,28126,28127,28128,28129,28130,28131,28132,28133,28134,28135,28136,28137,28138,28139,28140,28141,28142,28143,28144,28145,28146,28147,28148,28149,28150,28151,28152,28153,28154,28155,28156,28157,28158,28159,28160,28161,28162,28163,28164,28165,28166,28167,28168,28169,28170,28171,28172,28173,28174,28175,28176,28177,28178,28179,28180,28181,28182,28183,28184,28185,28186,28187,28188,28189,28190,28191,28192,28193,28194,28195,28196,28197,28198,28199,28200,28201,28202,28203,28204,28205,28206,28207,28208,28209,28210,28211,28212,28213,28214,28215,28216,28217,28218,28219,28220,28221,28222,28223,28224,28225,28226,28227,28228,28229,28230,28231,28232,28233,28234,28235,28236,28237,28238,28239,28240,28241,28242,28243,28244,28245,28246,28247,28248,28249,28250,28251,28252,28253,28254,28255,28256,28257,28258,28259,28260,28261,28262,28263,28264,28265,28266,28267,28268,28269,28270,28271,28272,28273,28274,28275,28276,28277,28278,28279,28280,28281,28282,28283,28284,28285,28286,28287,28288,28289,28290,28291,28292,28293,28294,28295,28296,28297,28298,28299,28300,28301,28302,28303,28304,28305,28306,28307,28308,28309,28310,28311,28312,28313,28314,28315,28316,28317,28318,28319,28320,28321,28322,28323,28324,28325,28326,28327,28328,28329,28330,28331,28332,28333,28334,28335,28336,28337,28338,28339,28340,28341,28342,28343,28344,28345,28346,28347,28348,28349,28350,28351,28352,28353,28354,28355,28356,28357,28358,28359,28360,28361,28362,28363,28364,28365,28366,28367,28368,28369,28370,28371,28372,28373,28374,28375,28376,28377,28378,28379,28380,28381,28382,28383,28384,28385,28386,28387,28388,28389,28390,28391,28392,28393,28394,28395,28396,28397,28398,28399,28400,28401,28402,28403,28404,28405,28406,28407,28408,28409,28410,28411,28412,28413,28414,28415,28416,28417,28418,28419,28420,28421,28422,28423,28424,28425,28426,28427,28428,28429,28430,28431,28432,28433,28434,28435,28436,28437,28438,28439,28440,28441,28442,28443,28444,28445,28446,28447,28448,28449,28450,28451,28452,28453,28454,28455,28456,28457,28458,28459,28460,28461,28462,28463,28464,28465,28466,28467,28468,28469,28470,28471,28472,28473,28474,28475,28476,28477,28478,28479,28480,28481,28482,28483,28484,28485,28486,28487,28488,28489,28490,28491,28492,28493,28494,28495,28496,28497,28498,28499,28500,28501,28502,28503,28504,28505,28506,28507,28508,28509,28510,28511,28512,28513,28514,28515,28516,28517,28518,28519,28520,28521,28522,28523,28524,28525,28526,28527,28528,28529,28530,28531,28532,28533,28534,28535,28536,28537,28538,28539,28540,28541,28542,28543,28544,28545,28546,28547,28548,28549,28550,28551,28552,28553,28554,28555,28556,28557,28558,28559,28560,28561,28562,28563,28564,28565,28566,28567,28568,28569,28570,28571,28572,28573,28574,28575,28576,28577,28578,28579,28580,28581,28582,28583,28584,28585,28586,28587,28588,28589,28590,28591,28592,28593,28594,28595,28596,28597,28598,28599,28600,28601,28602,28603,28604,28605,28606,28607,28608,28609,28610,28611,28612,28613,28614,28615,28616,28617,28618,28619,28620,28621,28622,28623,28624,28625,28626,28627,28628,28629,28630,28631,28632,28633,28634,28635,28636,28637,28638,28639,28640,28641,28642,28643,28644,28645,28646,28647,28648,28649,28650,28651,28652,28653,28654,28655,28656,28657,28658,28659,28660,28661,28662,28663,28664,28665,28666,28667,28668,28669,28670,28671,28672,28673,28674,28675,28676,28677,28678,28679,28680,28681,28682,28683,28684,28685,28686,28687,28688,28689,28690,28691,28692,28693,28694,28695,28696,28697,28698,28699,28700,28701,28702,28703,28704,28705,28706,28707,28708,28709,28710,28711,28712,28713,28714,28715,28716,28717,28718,28719,28720,28721,28722,28723,28724,28725,28726,28727,28728,28729,28730,28731,28732,28733,28734,28735,28736,28737,28738,28739,28740,28741,28742,28743,28744,28745,28746,28747,28748,28749,28750,28751,28752,28753,28754,28755,28756,28757,28758,28759,28760,28761,28762,28763,28764,28765,28766,28767,28768,28769,28770,28771,28772,28773,28774,28775,28776,28777,28778,28779,28780,28781,28782,28783,28784,28785,28786,28787,28788,28789,28790,28791,28792,28793,28794,28795,28796,28797,28798,28799,28800,28801,28802,28803,28804,28805,28806,28807,28808,28809,28810,28811,28812,28813,28814,28815,28816,28817,28818,28819,28820,28821,28822,28823,28824,28825,28826,28827,28828,28829,28830,28831,28832,28833,28834,28835,28836,28837,28838,28839,28840,28841,28842,28843,28844,28845,28846,28847,28848,28849,28850,28851,28852,28853,28854,28855,28856,28857,28858,28859,28860,28861,28862,28863,28864,28865,28866,28867,28868,28869,28870,28871,28872,28873,28874,28875,28876,28877,28878,28879,28880,28881,28882,28883,28884,28885,28886,28887,28888,28889,28890,28891,28892,28893,28894,28895,28896,28897,28898,28899,28900,28901,28902,28903,28904,28905,28906,28907,28908,28909,28910,28911,28912,28913,28914,28915,28916,28917,28918,28919,28920,28921,28922,28923,28924,28925,28926,28927,28928,28929,28930,28931,28932,28933,28934,28935,28936,28937,28938,28939,28940,28941,28942,28943,28944,28945,28946,28947,28948,28949,28950,28951,28952,28953,28954,28955,28956,28957,28958,28959,28960,28961,28962,28963,28964,28965,28966,28967,28968,28969,28970,28971,28972,28973,28974,28975,28976,28977,28978,28979,28980,28981,28982,28983,28984,28985,28986,28987,28988,28989,28990,28991,28992,28993,28994,28995,28996,28997,28998,28999,29000,29001,29002,29003,29004,29005,29006,29007,29008,29009,29010,29011,29012,29013,29014,29015,29016,29017,29018,29019,29020,29021,29022,29023,29024,29025,29026,29027,29028,29029,29030,29031,29032,29033,29034,29035,29036,29037,29038,29039,29040,29041,29042,29043,29044,29045,29046,29047,29048,29049,29050,29051,29052,29053,29054,29055,29056,29057,29058,29059,29060,29061,29062,29063,29064,29065,29066,29067,29068,29069,29070,29071,29072,29073,29074,29075,29076,29077,29078,29079,29080,29081,29082,29083,29084,29085,29086,29087,29088,29089,29090,29091,29092,29093,29094,29095,29096,29097,29098,29099,29100,29101,29102,29103,29104,29105,29106,29107,29108,29109,29110,29111,29112,29113,29114,29115,29116,29117,29118,29119,29120,29121,29122,29123,29124,29125,29126,29127,29128,29129,29130,29131,29132,29133,29134,29135,29136,29137,29138,29139,29140,29141,29142,29143,29144,29145,29146,29147,29148,29149,29150,29151,29152,29153,29154,29155,29156,29157,29158,29159,29160,29161,29162,29163,29164,29165,29166,29167,29168,29169,29170,29171,29172,29173,29174,29175,29176,29177,29178,29179,29180,29181,29182,29183,29184,29185,29186,29187,29188,29189,29190,29191,29192,29193,29194,29195,29196,29197,29198,29199,29200,29201,29202,29203,29204,29205,29206,29207,29208,29209,29210,29211,29212,29213,29214,29215,29216,29217,29218,29219,29220,29221,29222,29223,29224,29225,29226,29227,29228,29229,29230,29231,29232,29233,29234,29235,29236,29237,29238,29239,29240,29241,29242,29243,29244,29245,29246,29247,29248,29249,29250,29251,29252,29253,29254,29255,29256,29257,29258,29259,29260,29261,29262,29263,29264,29265,29266,29267,29268,29269,29270,29271,29272,29273,29274,29275,29276,29277,29278,29279,29280,29281,29282,29283,29284,29285,29286,29287,29288,29289,29290,29291,29292,29293,29294,29295,29296,29297,29298,29299,29300,29301,29302,29303,29304,29305,29306,29307,29308,29309,29310,29311,29312,29313,29314,29315,29316,29317,29318,29319,29320,29321,29322,29323,29324,29325,29326,29327,29328,29329,29330,29331,29332,29333,29334,29335,29336,29337,29338,29339,29340,29341,29342,29343,29344,29345,29346,29347,29348,29349,29350,29351,29352,29353,29354,29355,29356,29357,29358,29359,29360,29361,29362,29363,29364,29365,29366,29367,29368,29369,29370,29371,29372,29373,29374,29375,29376,29377,29378,29379,29380,29381,29382,29383,29384,29385,29386,29387,29388,29389,29390,29391,29392,29393,29394,29395,29396,29397,29398,29399,29400,29401,29402,29403,29404,29405,29406,29407,29408,29409,29410,29411,29412,29413,29414,29415,29416,29417,29418,29419,29420,29421,29422,29423,29424,29425,29426,29427,29428,29429,29430,29431,29432,29433,29434,29435,29436,29437,29438,29439,29440,29441,29442,29443,29444,29445,29446,29447,29448,29449,29450,29451,29452,29453,29454,29455,29456,29457,29458,29459,29460,29461,29462,29463,29464,29465,29466,29467,29468,29469,29470,29471,29472,29473,29474,29475,29476,29477,29478,29479,29480,29481,29482,29483,29484,29485,29486,29487,29488,29489,29490,29491,29492,29493,29494,29495,29496,29497,29498,29499,29500,29501,29502,29503,29504,29505,29506,29507,29508,29509,29510,29511,29512,29513,29514,29515,29516,29517,29518,29519,29520,29521,29522,29523,29524,29525,29526,29527,29528,29529,29530,29531,29532,29533,29534,29535,29536,29537,29538,29539,29540,29541,29542,29543,29544,29545,29546,29547,29548,29549,29550,29551,29552,29553,29554,29555,29556,29557,29558,29559,29560,29561,29562,29563,29564,29565,29566,29567,29568,29569,29570,29571,29572,29573,29574,29575,29576,29577,29578,29579,29580,29581,29582,29583,29584,29585,29586,29587,29588,29589,29590,29591,29592,29593,29594,29595,29596,29597,29598,29599,29600,29601,29602,29603,29604,29605,29606,29607,29608,29609,29610,29611,29612,29613,29614,29615,29616,29617,29618,29619,29620,29621,29622,29623,29624,29625,29626,29627,29628,29629,29630,29631,29632,29633,29634,29635,29636,29637,29638,29639,29640,29641,29642,29643,29644,29645,29646,29647,29648,29649,29650,29651,29652,29653,29654,29655,29656,29657,29658,29659,29660,29661,29662,29663,29664,29665,29666,29667,29668,29669,29670,29671,29672,29673,29674,29675,29676,29677,29678,29679,29680,29681,29682,29683,29684,29685,29686,29687,29688,29689,29690,29691,29692,29693,29694,29695,29696,29697,29698,29699,29700,29701,29702,29703,29704,29705,29706,29707,29708,29709,29710,29711,29712,29713,29714,29715,29716,29717,29718,29719,29720,29721,29722,29723,29724,29725,29726,29727,29728,29729,29730,29731,29732,29733,29734,29735,29736,29737,29738,29739,29740,29741,29742,29743,29744,29745,29746,29747,29748,29749,29750,29751,29752,29753,29754,29755,29756,29757,29758,29759,29760,29761,29762,29763,29764,29765,29766,29767,29768,29769,29770,29771,29772,29773,29774,29775,29776,29777,29778,29779,29780,29781,29782,29783,29784,29785,29786,29787,29788,29789,29790,29791,29792,29793,29794,29795,29796,29797,29798,29799,29800,29801,29802,29803,29804,29805,29806,29807,29808,29809,29810,29811,29812,29813,29814,29815,29816,29817,29818,29819,29820,29821,29822,29823,29824,29825,29826,29827,29828,29829,29830,29831,29832,29833,29834,29835,29836,29837,29838,29839,29840,29841,29842,29843,29844,29845,29846,29847,29848,29849,29850,29851,29852,29853,29854,29855,29856,29857,29858,29859,29860,29861,29862,29863,29864,29865,29866,29867,29868,29869,29870,29871,29872,29873,29874,29875,29876,29877,29878,29879,29880,29881,29882,29883,29884,29885,29886,29887,29888,29889,29890,29891,29892,29893,29894,29895,29896,29897,29898,29899,29900,29901,29902,29903,29904,29905,29906,29907,29908,29909,29910,29911,29912,29913,29914,29915,29916,29917,29918,29919,29920,29921,29922,29923,29924,29925,29926,29927,29928,29929,29930,29931,29932,29933,29934,29935,29936,29937,29938,29939,29940,29941,29942,29943,29944,29945,29946,29947,29948,29949,29950,29951,29952,29953,29954,29955,29956,29957,29958,29959,29960,29961,29962,29963,29964,29965,29966,29967,29968,29969,29970,29971,29972,29973,29974,29975,29976,29977,29978,29979,29980,29981,29982,29983,29984,29985,29986,29987,29988,29989,29990,29991,29992,29993,29994,29995,29996,29997,29998,29999,30000,30001,30002,30003,30004,30005,30006,30007,30008,30009,30010,30011,30012,30013,30014,30015,30016,30017,30018,30019,30020,30021,30022,30023,30024,30025,30026,30027,30028,30029,30030,30031,30032,30033,30034,30035,30036,30037,30038,30039,30040,30041,30042,30043,30044,30045,30046,30047,30048,30049,30050,30051,30052,30053,30054,30055,30056,30057,30058,30059,30060,30061,30062,30063,30064,30065,30066,30067,30068,30069,30070,30071,30072,30073,30074,30075,30076,30077,30078,30079,30080,30081,30082,30083,30084,30085,30086,30087,30088,30089,30090,30091,30092,30093,30094,30095,30096,30097,30098,30099,30100,30101,30102,30103,30104,30105,30106,30107,30108,30109,30110,30111,30112,30113,30114,30115,30116,30117,30118,30119,30120,30121,30122,30123,30124,30125,30126,30127,30128,30129,30130,30131,30132,30133,30134,30135,30136,30137,30138,30139,30140,30141,30142,30143,30144,30145,30146,30147,30148,30149,30150,30151,30152,30153,30154,30155,30156,30157,30158,30159,30160,30161,30162,30163,30164,30165,30166,30167,30168,30169,30170,30171,30172,30173,30174,30175,30176,30177,30178,30179,30180,30181,30182,30183,30184,30185,30186,30187,30188,30189,30190,30191,30192,30193,30194,30195,30196,30197,30198,30199,30200,30201,30202,30203,30204,30205,30206,30207,30208,30209,30210,30211,30212,30213,30214,30215,30216,30217,30218,30219,30220,30221,30222,30223,30224,30225,30226,30227,30228,30229,30230,30231,30232,30233,30234,30235,30236,30237,30238,30239,30240,30241,30242,30243,30244,30245,30246,30247,30248,30249,30250,30251,30252,30253,30254,30255,30256,30257,30258,30259,30260,30261,30262,30263,30264,30265,30266,30267,30268,30269,30270,30271,30272,30273,30274,30275,30276,30277,30278,30279,30280,30281,30282,30283,30284,30285,30286,30287,30288,30289,30290,30291,30292,30293,30294,30295,30296,30297,30298,30299,30300,30301,30302,30303,30304,30305,30306,30307,30308,30309,30310,30311,30312,30313,30314,30315,30316,30317,30318,30319,30320,30321,30322,30323,30324,30325,30326,30327,30328,30329,30330,30331,30332,30333,30334,30335,30336,30337,30338,30339,30340,30341,30342,30343,30344,30345,30346,30347,30348,30349,30350,30351,30352,30353,30354,30355,30356,30357,30358,30359,30360,30361,30362,30363,30364,30365,30366,30367,30368,30369,30370,30371,30372,30373,30374,30375,30376,30377,30378,30379,30380,30381,30382,30383,30384,30385,30386,30387,30388,30389,30390,30391,30392,30393,30394,30395,30396,30397,30398,30399,30400,30401,30402,30403,30404,30405,30406,30407,30408,30409,30410,30411,30412,30413,30414,30415,30416,30417,30418,30419,30420,30421,30422,30423,30424,30425,30426,30427,30428,30429,30430,30431,30432,30433,30434,30435,30436,30437,30438,30439,30440,30441,30442,30443,30444,30445,30446,30447,30448,30449,30450,30451,30452,30453,30454,30455,30456,30457,30458,30459,30460,30461,30462,30463,30464,30465,30466,30467,30468,30469,30470,30471,30472,30473,30474,30475,30476,30477,30478,30479,30480,30481,30482,30483,30484,30485,30486,30487,30488,30489,30490,30491,30492,30493,30494,30495,30496,30497,30498,30499,30500,30501,30502,30503,30504,30505,30506,30507,30508,30509,30510,30511,30512,30513,30514,30515,30516,30517,30518,30519,30520,30521,30522,30523,30524,30525,30526,30527,30528,30529,30530,30531,30532,30533,30534,30535,30536,30537,30538,30539,30540,30541,30542,30543,30544,30545,30546,30547,30548,30549,30550,30551,30552,30553,30554,30555,30556,30557,30558,30559,30560,30561,30562,30563,30564,30565,30566,30567,30568,30569,30570,30571,30572,30573,30574,30575,30576,30577,30578,30579,30580,30581,30582,30583,30584,30585,30586,30587,30588,30589,30590,30591,30592,30593,30594,30595,30596,30597,30598,30599,30600,30601,30602,30603,30604,30605,30606,30607,30608,30609,30610,30611,30612,30613,30614,30615,30616,30617,30618,30619,30620,30621,30622,30623,30624,30625,30626,30627,30628,30629,30630,30631,30632,30633,30634,30635,30636,30637,30638,30639,30640,30641,30642,30643,30644,30645,30646,30647,30648,30649,30650,30651,30652,30653,30654,30655,30656,30657,30658,30659,30660,30661,30662,30663,30664,30665,30666,30667,30668,30669,30670,30671,30672,30673,30674,30675,30676,30677,30678,30679,30680,30681,30682,30683,30684,30685,30686,30687,30688,30689,30690,30691,30692,30693,30694,30695,30696,30697,30698,30699,30700,30701,30702,30703,30704,30705,30706,30707,30708,30709,30710,30711,30712,30713,30714,30715,30716,30717,30718,30719,30720,30721,30722,30723,30724,30725,30726,30727,30728,30729,30730,30731,30732,30733,30734,30735,30736,30737,30738,30739,30740,30741,30742,30743,30744,30745,30746,30747,30748,30749,30750,30751,30752,30753,30754,30755,30756,30757,30758,30759,30760,30761,30762,30763,30764,30765,30766,30767,30768,30769,30770,30771,30772,30773,30774,30775,30776,30777,30778,30779,30780,30781,30782,30783,30784,30785,30786,30787,30788,30789,30790,30791,30792,30793,30794,30795,30796,30797,30798,30799,30800,30801,30802,30803,30804,30805,30806,30807,30808,30809,30810,30811,30812,30813,30814,30815,30816,30817,30818,30819,30820,30821,30822,30823,30824,30825,30826,30827,30828,30829,30830,30831,30832,30833,30834,30835,30836,30837,30838,30839,30840,30841,30842,30843,30844,30845,30846,30847,30848,30849,30850,30851,30852,30853,30854,30855,30856,30857,30858,30859,30860,30861,30862,30863,30864,30865,30866,30867,30868,30869,30870,30871,30872,30873,30874,30875,30876,30877,30878,30879,30880,30881,30882,30883,30884,30885,30886,30887,30888,30889,30890,30891,30892,30893,30894,30895,30896,30897,30898,30899,30900,30901,30902,30903,30904,30905,30906,30907,30908,30909,30910,30911,30912,30913,30914,30915,30916,30917,30918,30919,30920,30921,30922,30923,30924,30925,30926,30927,30928,30929,30930,30931,30932,30933,30934,30935,30936,30937,30938,30939,30940,30941,30942,30943,30944,30945,30946,30947,30948,30949,30950,30951,30952,30953,30954,30955,30956,30957,30958,30959,30960,30961,30962,30963,30964,30965,30966,30967,30968,30969,30970,30971,30972,30973,30974,30975,30976,30977,30978,30979,30980,30981,30982,30983,30984,30985,30986,30987,30988,30989,30990,30991,30992,30993,30994,30995,30996,30997,30998,30999,31000,31001,31002,31003,31004,31005,31006,31007,31008,31009,31010,31011,31012,31013,31014,31015,31016,31017,31018,31019,31020,31021,31022,31023,31024,31025,31026,31027,31028,31029,31030,31031,31032,31033,31034,31035,31036,31037,31038,31039,31040,31041,31042,31043,31044,31045,31046,31047,31048,31049,31050,31051,31052,31053,31054,31055,31056,31057,31058,31059,31060,31061,31062,31063,31064,31065,31066,31067,31068,31069,31070,31071,31072,31073,31074,31075,31076,31077,31078,31079,31080,31081,31082,31083,31084,31085,31086,31087,31088,31089,31090,31091,31092,31093,31094,31095,31096,31097,31098,31099,31100,31101,31102,31103,31104,31105,31106,31107,31108,31109,31110,31111,31112,31113,31114,31115,31116,31117,31118,31119,31120,31121,31122,31123,31124,31125,31126,31127,31128,31129,31130,31131,31132,31133,31134,31135,31136,31137,31138,31139,31140,31141,31142,31143,31144,31145,31146,31147,31148,31149,31150,31151,31152,31153,31154,31155,31156,31157,31158,31159,31160,31161,31162,31163,31164,31165,31166,31167,31168,31169,31170,31171,31172,31173,31174,31175,31176,31177,31178,31179,31180,31181,31182,31183,31184,31185,31186,31187,31188,31189,31190,31191,31192,31193,31194,31195,31196,31197,31198,31199,31200,31201,31202,31203,31204,31205,31206,31207,31208,31209,31210,31211,31212,31213,31214,31215,31216,31217,31218,31219,31220,31221,31222,31223,31224,31225,31226,31227,31228,31229,31230,31231,31232,31233,31234,31235,31236,31237,31238,31239,31240,31241,31242,31243,31244,31245,31246,31247,31248,31249,31250,31251,31252,31253,31254,31255,31256,31257,31258,31259,31260,31261,31262,31263,31264,31265,31266,31267,31268,31269,31270,31271,31272,31273,31274,31275,31276,31277,31278,31279,31280,31281,31282,31283,31284,31285,31286,31287,31288,31289,31290,31291,31292,31293,31294,31295,31296,31297,31298,31299,31300,31301,31302,31303,31304,31305,31306,31307,31308,31309,31310,31311,31312,31313,31314,31315,31316,31317,31318,31319,31320,31321,31322,31323,31324,31325,31326,31327,31328,31329,31330,31331,31332,31333,31334,31335,31336,31337,31338,31339,31340,31341,31342,31343,31344,31345,31346,31347,31348,31349,31350,31351,31352,31353,31354,31355,31356,31357,31358,31359,31360,31361,31362,31363,31364,31365,31366,31367,31368,31369,31370,31371,31372,31373,31374,31375,31376,31377,31378,31379,31380,31381,31382,31383,31384,31385,31386,31387,31388,31389,31390,31391,31392,31393,31394,31395,31396,31397,31398,31399,31400,31401,31402,31403,31404,31405,31406,31407,31408,31409,31410,31411,31412,31413,31414,31415,31416,31417,31418,31419,31420,31421,31422,31423,31424,31425,31426,31427,31428,31429,31430,31431,31432,31433,31434,31435,31436,31437,31438,31439,31440,31441,31442,31443,31444,31445,31446,31447,31448,31449,31450,31451,31452,31453,31454,31455,31456,31457,31458,31459,31460,31461,31462,31463,31464,31465,31466,31467,31468,31469,31470,31471,31472,31473,31474,31475,31476,31477,31478,31479,31480,31481,31482,31483,31484,31485,31486,31487,31488,31489,31490,31491,31492,31493,31494,31495,31496,31497,31498,31499,31500,31501,31502,31503,31504,31505,31506,31507,31508,31509,31510,31511,31512,31513,31514,31515,31516,31517,31518,31519,31520,31521,31522,31523,31524,31525,31526,31527,31528,31529,31530,31531,31532,31533,31534,31535,31536,31537,31538,31539,31540,31541,31542,31543,31544,31545,31546,31547,31548,31549,31550,31551,31552,31553,31554,31555,31556,31557,31558,31559,31560,31561,31562,31563,31564,31565,31566,31567,31568,31569,31570,31571,31572,31573,31574,31575,31576,31577,31578,31579,31580,31581,31582,31583,31584,31585,31586,31587,31588,31589,31590,31591,31592,31593,31594,31595,31596,31597,31598,31599,31600,31601,31602,31603,31604,31605,31606,31607,31608,31609,31610,31611,31612,31613,31614,31615,31616,31617,31618,31619,31620,31621,31622,31623,31624,31625,31626,31627,31628,31629,31630,31631,31632,31633,31634,31635,31636,31637,31638,31639,31640,31641,31642,31643,31644,31645,31646,31647,31648,31649,31650,31651,31652,31653,31654,31655,31656,31657,31658,31659,31660,31661,31662,31663,31664,31665,31666,31667,31668,31669,31670,31671,31672,31673,31674,31675,31676,31677,31678,31679,31680,31681,31682,31683,31684,31685,31686,31687,31688,31689,31690,31691,31692,31693,31694,31695,31696,31697,31698,31699,31700,31701,31702,31703,31704,31705,31706,31707,31708,31709,31710,31711,31712,31713,31714,31715,31716,31717,31718,31719,31720,31721,31722,31723,31724,31725,31726,31727,31728,31729,31730,31731,31732,31733,31734,31735,31736,31737,31738,31739,31740,31741,31742,31743,31744,31745,31746,31747,31748,31749,31750,31751,31752,31753,31754,31755,31756,31757,31758,31759,31760,31761,31762,31763,31764,31765,31766,31767,31768,31769,31770,31771,31772,31773,31774,31775,31776,31777,31778,31779,31780,31781,31782,31783,31784,31785,31786,31787,31788,31789,31790,31791,31792,31793,31794,31795,31796,31797,31798,31799,31800,31801,31802,31803,31804,31805,31806,31807,31808,31809,31810,31811,31812,31813,31814,31815,31816,31817,31818,31819,31820,31821,31822,31823,31824,31825,31826,31827,31828,31829,31830,31831,31832,31833,31834,31835,31836,31837,31838,31839,31840,31841,31842,31843,31844,31845,31846,31847,31848,31849,31850,31851,31852,31853,31854,31855,31856,31857,31858,31859,31860,31861,31862,31863,31864,31865,31866,31867,31868,31869,31870,31871,31872,31873,31874,31875,31876,31877,31878,31879,31880,31881,31882,31883,31884,31885,31886,31887,31888,31889,31890,31891,31892,31893,31894,31895,31896,31897,31898,31899,31900,31901,31902,31903,31904,31905,31906,31907,31908,31909,31910,31911,31912,31913,31914,31915,31916,31917,31918,31919,31920,31921,31922,31923,31924,31925,31926,31927,31928,31929,31930,31931,31932,31933,31934,31935,31936,31937,31938,31939,31940,31941,31942,31943,31944,31945,31946,31947,31948,31949,31950,31951,31952,31953,31954,31955,31956,31957,31958,31959,31960,31961,31962,31963,31964,31965,31966,31967,31968,31969,31970,31971,31972,31973,31974,31975,31976,31977,31978,31979,31980,31981,31982,31983,31984,31985,31986,31987,31988,31989,31990,31991,31992,31993,31994,31995,31996,31997,31998,31999,32000,32001,32002,32003,32004,32005,32006,32007,32008,32009,32010,32011,32012,32013,32014,32015,32016,32017,32018,32019,32020,32021,32022,32023,32024,32025,32026,32027,32028,32029,32030,32031,32032,32033,32034,32035,32036,32037,32038,32039,32040,32041,32042,32043,32044,32045,32046,32047,32048,32049,32050,32051,32052,32053,32054,32055,32056,32057,32058,32059,32060,32061,32062,32063,32064,32065,32066,32067,32068,32069,32070,32071,32072,32073,32074,32075,32076,32077,32078,32079,32080,32081,32082,32083,32084,32085,32086,32087,32088,32089,32090,32091,32092,32093,32094,32095,32096,32097,32098,32099,32100,32101,32102,32103,32104,32105,32106,32107,32108,32109,32110,32111,32112,32113,32114,32115,32116,32117,32118,32119,32120,32121,32122,32123,32124,32125,32126,32127,32128,32129,32130,32131,32132,32133,32134,32135,32136,32137,32138,32139,32140,32141,32142,32143,32144,32145,32146,32147,32148,32149,32150,32151,32152,32153,32154,32155,32156,32157,32158,32159,32160,32161,32162,32163,32164,32165,32166,32167,32168,32169,32170,32171,32172,32173,32174,32175,32176,32177,32178,32179,32180,32181,32182,32183,32184,32185,32186,32187,32188,32189,32190,32191,32192,32193,32194,32195,32196,32197,32198,32199,32200,32201,32202,32203,32204,32205,32206,32207,32208,32209,32210,32211,32212,32213,32214,32215,32216,32217,32218,32219,32220,32221,32222,32223,32224,32225,32226,32227,32228,32229,32230,32231,32232,32233,32234,32235,32236,32237,32238,32239,32240,32241,32242,32243,32244,32245,32246,32247,32248,32249,32250,32251,32252,32253,32254,32255,32256,32257,32258,32259,32260,32261,32262,32263,32264,32265,32266,32267,32268,32269,32270,32271,32272,32273,32274,32275,32276,32277,32278,32279,32280,32281,32282,32283,32284,32285,32286,32287,32288,32289,32290,32291,32292,32293,32294,32295,32296,32297,32298,32299,32300,32301,32302,32303,32304,32305,32306,32307,32308,32309,32310,32311,32312,32313,32314,32315,32316,32317,32318,32319,32320,32321,32322,32323,32324,32325,32326,32327,32328,32329,32330,32331,32332,32333,32334,32335,32336,32337,32338,32339,32340,32341,32342,32343,32344,32345,32346,32347,32348,32349,32350,32351,32352,32353,32354,32355,32356,32357,32358,32359,32360,32361,32362,32363,32364,32365,32366,32367,32368,32369,32370,32371,32372,32373,32374,32375,32376,32377,32378,32379,32380,32381,32382,32383,32384,32385,32386,32387,32388,32389,32390,32391,32392,32393,32394,32395,32396,32397,32398,32399,32400,32401,32402,32403,32404,32405,32406,32407,32408,32409,32410,32411,32412,32413,32414,32415,32416,32417,32418,32419,32420,32421,32422,32423,32424,32425,32426,32427,32428,32429,32430,32431,32432,32433,32434,32435,32436,32437,32438,32439,32440,32441,32442,32443,32444,32445,32446,32447,32448,32449,32450,32451,32452,32453,32454,32455,32456,32457,32458,32459,32460,32461,32462,32463,32464,32465,32466,32467,32468,32469,32470,32471,32472,32473,32474,32475,32476,32477,32478,32479,32480,32481,32482,32483,32484,32485,32486,32487,32488,32489,32490,32491,32492,32493,32494,32495,32496,32497,32498,32499,32500,32501,32502,32503,32504,32505,32506,32507,32508,32509,32510,32511,32512,32513,32514,32515,32516,32517,32518,32519,32520,32521,32522,32523,32524,32525,32526,32527,32528,32529,32530,32531,32532,32533,32534,32535,32536,32537,32538,32539,32540,32541,32542,32543,32544,32545,32546,32547,32548,32549,32550,32551,32552,32553,32554,32555,32556,32557,32558,32559,32560,32561,32562,32563,32564,32565,32566,32567,32568,32569,32570,32571,32572,32573,32574,32575,32576,32577,32578,32579,32580,32581,32582,32583,32584,32585,32586,32587,32588,32589,32590,32591,32592,32593,32594,32595,32596,32597,32598,32599,32600,32601,32602,32603,32604,32605,32606,32607,32608,32609,32610,32611,32612,32613,32614,32615,32616,32617,32618,32619,32620,32621,32622,32623,32624,32625,32626,32627,32628,32629,32630,32631,32632,32633,32634,32635,32636,32637,32638,32639,32640,32641,32642,32643,32644,32645,32646,32647,32648,32649,32650,32651,32652,32653,32654,32655,32656,32657,32658,32659,32660,32661,32662,32663,32664,32665,32666,32667,32668,32669,32670,32671,32672,32673,32674,32675,32676,32677,32678,32679,32680,32681,32682,32683,32684,32685,32686,32687,32688,32689,32690,32691,32692,32693,32694,32695,32696,32697,32698,32699,32700,32701,32702,32703,32704,32705,32706,32707,32708,32709,32710,32711,32712,32713,32714,32715,32716,32717,32718,32719,32720,32721,32722,32723,32724,32725,32726,32727,32728,32729,32730,32731,32732,32733,32734,32735,32736,32737,32738,32739,32740,32741,32742,32743,32744,32745,32746,32747,32748,32749,32750,32751,32752,32753,32754,32755,32756,32757,32758,32759,32760,32761,32762,32763,32764,32765,32766,32767,32768,32769,32770,32771,32772,32773,32774,32775,32776,32777,32778,32779,32780,32781,32782,32783,32784,32785,32786,32787,32788,32789,32790,32791,32792,32793,32794,32795,32796,32797,32798,32799,32800,32801,32802,32803,32804,32805,32806,32807,32808,32809,32810,32811,32812,32813,32814,32815,32816,32817,32818,32819,32820,32821,32822,32823,32824,32825,32826,32827,32828,32829,32830,32831,32832,32833,32834,32835,32836,32837,32838,32839,32840,32841,32842,32843,32844,32845,32846,32847,32848,32849,32850,32851,32852,32853,32854,32855,32856,32857,32858,32859,32860,32861,32862,32863,32864,32865,32866,32867,32868,32869,32870,32871,32872,32873,32874,32875,32876,32877,32878,32879,32880,32881,32882,32883,32884,32885,32886,32887,32888,32889,32890,32891,32892,32893,32894,32895,32896,32897,32898,32899,32900,32901,32902,32903,32904,32905,32906,32907,32908,32909,32910,32911,32912,32913,32914,32915,32916,32917,32918,32919,32920,32921,32922,32923,32924,32925,32926,32927,32928,32929,32930,32931,32932,32933,32934,32935,32936,32937,32938,32939,32940,32941,32942,32943,32944,32945,32946,32947,32948,32949,32950,32951,32952,32953,32954,32955,32956,32957,32958,32959,32960,32961,32962,32963,32964,32965,32966,32967,32968,32969,32970,32971,32972,32973,32974,32975,32976,32977,32978,32979,32980,32981,32982,32983,32984,32985,32986,32987,32988,32989,32990,32991,32992,32993,32994,32995,32996,32997,32998,32999,33000,33001,33002,33003,33004,33005,33006,33007,33008,33009,33010,33011,33012,33013,33014,33015,33016,33017,33018,33019,33020,33021,33022,33023,33024,33025,33026,33027,33028,33029,33030,33031,33032,33033,33034,33035,33036,33037,33038,33039,33040,33041,33042,33043,33044,33045,33046,33047,33048,33049,33050,33051,33052,33053,33054,33055,33056,33057,33058,33059,33060,33061,33062,33063,33064,33065,33066,33067,33068,33069,33070,33071,33072,33073,33074,33075,33076,33077,33078,33079,33080,33081,33082,33083,33084,33085,33086,33087,33088,33089,33090,33091,33092,33093,33094,33095,33096,33097,33098,33099,33100,33101,33102,33103,33104,33105,33106,33107,33108,33109,33110,33111,33112,33113,33114,33115,33116,33117,33118,33119,33120,33121,33122,33123,33124,33125,33126,33127,33128,33129,33130,33131,33132,33133,33134,33135,33136,33137,33138,33139,33140,33141,33142,33143,33144,33145,33146,33147,33148,33149,33150,33151,33152,33153,33154,33155,33156,33157,33158,33159,33160,33161,33162,33163,33164,33165,33166,33167,33168,33169,33170,33171,33172,33173,33174,33175,33176,33177,33178,33179,33180,33181,33182,33183,33184,33185,33186,33187,33188,33189,33190,33191,33192,33193,33194,33195,33196,33197,33198,33199,33200,33201,33202,33203,33204,33205,33206,33207,33208,33209,33210,33211,33212,33213,33214,33215,33216,33217,33218,33219,33220,33221,33222,33223,33224,33225,33226,33227,33228,33229,33230,33231,33232,33233,33234,33235,33236,33237,33238,33239,33240,33241,33242,33243,33244,33245,33246,33247,33248,33249,33250,33251,33252,33253,33254,33255,33256,33257,33258,33259,33260,33261,33262,33263,33264,33265,33266,33267,33268,33269,33270,33271,33272,33273,33274,33275,33276,33277,33278,33279,33280,33281,33282,33283,33284,33285,33286,33287,33288,33289,33290,33291,33292,33293,33294,33295,33296,33297,33298,33299,33300,33301,33302,33303,33304,33305,33306,33307,33308,33309,33310,33311,33312,33313,33314,33315,33316,33317,33318,33319,33320,33321,33322,33323,33324,33325,33326,33327,33328,33329,33330,33331,33332,33333,33334,33335,33336,33337,33338,33339,33340,33341,33342,33343,33344,33345,33346,33347,33348,33349,33350,33351,33352,33353,33354,33355,33356,33357,33358,33359,33360,33361,33362,33363,33364,33365,33366,33367,33368,33369,33370,33371,33372,33373,33374,33375,33376,33377,33378,33379,33380,33381,33382,33383,33384,33385,33386,33387,33388,33389,33390,33391,33392,33393,33394,33395,33396,33397,33398,33399,33400,33401,33402,33403,33404,33405,33406,33407,33408,33409,33410,33411,33412,33413,33414,33415,33416,33417,33418,33419,33420,33421,33422,33423,33424,33425,33426,33427,33428,33429,33430,33431,33432,33433,33434,33435,33436,33437,33438,33439,33440,33441,33442,33443,33444,33445,33446,33447,33448,33449,33450,33451,33452,33453,33454,33455,33456,33457,33458,33459,33460,33461,33462,33463,33464,33465,33466,33467,33468,33469,33470,33471,33472,33473,33474,33475,33476,33477,33478,33479,33480,33481,33482,33483,33484,33485,33486,33487,33488,33489,33490,33491,33492,33493,33494,33495,33496,33497,33498,33499,33500,33501,33502,33503,33504,33505,33506,33507,33508,33509,33510,33511,33512,33513,33514,33515,33516,33517,33518,33519,33520,33521,33522,33523,33524,33525,33526,33527,33528,33529,33530,33531,33532,33533,33534,33535,33536,33537,33538,33539,33540,33541,33542,33543,33544,33545,33546,33547,33548,33549,33550,33551,33552,33553,33554,33555,33556,33557,33558,33559,33560,33561,33562,33563,33564,33565,33566,33567,33568,33569,33570,33571,33572,33573,33574,33575,33576,33577,33578,33579,33580,33581,33582,33583,33584,33585,33586,33587,33588,33589,33590,33591,33592,33593,33594,33595,33596,33597,33598,33599,33600,33601,33602,33603,33604,33605,33606,33607,33608,33609,33610,33611,33612,33613,33614,33615,33616,33617,33618,33619,33620,33621,33622,33623,33624,33625,33626,33627,33628,33629,33630,33631,33632,33633,33634,33635,33636,33637,33638,33639,33640,33641,33642,33643,33644,33645,33646,33647,33648,33649,33650,33651,33652,33653,33654,33655,33656,33657,33658,33659,33660,33661,33662,33663,33664,33665,33666,33667,33668,33669,33670,33671,33672,33673,33674,33675,33676,33677,33678,33679,33680,33681,33682,33683,33684,33685,33686,33687,33688,33689,33690,33691,33692,33693,33694,33695,33696,33697,33698,33699,33700,33701,33702,33703,33704,33705,33706,33707,33708,33709,33710,33711,33712,33713,33714,33715,33716,33717,33718,33719,33720,33721,33722,33723,33724,33725,33726,33727,33728,33729,33730,33731,33732,33733,33734,33735,33736,33737,33738,33739,33740,33741,33742,33743,33744,33745,33746,33747,33748,33749,33750,33751,33752,33753,33754,33755,33756,33757,33758,33759,33760,33761,33762,33763,33764,33765,33766,33767,33768,33769,33770,33771,33772,33773,33774,33775,33776,33777,33778,33779,33780,33781,33782,33783,33784,33785,33786,33787,33788,33789,33790,33791,33792,33793,33794,33795,33796,33797,33798,33799,33800,33801,33802,33803,33804,33805,33806,33807,33808,33809,33810,33811,33812,33813,33814,33815,33816,33817,33818,33819,33820,33821,33822,33823,33824,33825,33826,33827,33828,33829,33830,33831,33832,33833,33834,33835,33836,33837,33838,33839,33840,33841,33842,33843,33844,33845,33846,33847,33848,33849,33850,33851,33852,33853,33854,33855,33856,33857,33858,33859,33860,33861,33862,33863,33864,33865,33866,33867,33868,33869,33870,33871,33872,33873,33874,33875,33876,33877,33878,33879,33880,33881,33882,33883,33884,33885,33886,33887,33888,33889,33890,33891,33892,33893,33894,33895,33896,33897,33898,33899,33900,33901,33902,33903,33904,33905,33906,33907,33908,33909,33910,33911,33912,33913,33914,33915,33916,33917,33918,33919,33920,33921,33922,33923,33924,33925,33926,33927,33928,33929,33930,33931,33932,33933,33934,33935,33936,33937,33938,33939,33940,33941,33942,33943,33944,33945,33946,33947,33948,33949,33950,33951,33952,33953,33954,33955,33956,33957,33958,33959,33960,33961,33962,33963,33964,33965,33966,33967,33968,33969,33970,33971,33972,33973,33974,33975,33976,33977,33978,33979,33980,33981,33982,33983,33984,33985,33986,33987,33988,33989,33990,33991,33992,33993,33994,33995,33996,33997,33998,33999,34000,34001,34002,34003,34004,34005,34006,34007,34008,34009,34010,34011,34012,34013,34014,34015,34016,34017,34018,34019,34020,34021,34022,34023,34024,34025,34026,34027,34028,34029,34030,34031,34032,34033,34034,34035,34036,34037,34038,34039,34040,34041,34042,34043,34044,34045,34046,34047,34048,34049,34050,34051,34052,34053,34054,34055,34056,34057,34058,34059,34060,34061,34062,34063,34064,34065,34066,34067,34068,34069,34070,34071,34072,34073,34074,34075,34076,34077,34078,34079,34080,34081,34082,34083,34084,34085,34086,34087,34088,34089,34090,34091,34092,34093,34094,34095,34096,34097,34098,34099,34100,34101,34102,34103,34104,34105,34106,34107,34108,34109,34110,34111,34112,34113,34114,34115,34116,34117,34118,34119,34120,34121,34122,34123,34124,34125,34126,34127,34128,34129,34130,34131,34132,34133,34134,34135,34136,34137,34138,34139,34140,34141,34142,34143,34144,34145,34146,34147,34148,34149,34150,34151,34152,34153,34154,34155,34156,34157,34158,34159,34160,34161,34162,34163,34164,34165,34166,34167,34168,34169,34170,34171,34172,34173,34174,34175,34176,34177,34178,34179,34180,34181,34182,34183,34184,34185,34186,34187,34188,34189,34190,34191,34192,34193,34194,34195,34196,34197,34198,34199,34200,34201,34202,34203,34204,34205,34206,34207,34208,34209,34210,34211,34212,34213,34214,34215,34216,34217,34218,34219,34220,34221,34222,34223,34224,34225,34226,34227,34228,34229,34230,34231,34232,34233,34234,34235,34236,34237,34238,34239,34240,34241,34242,34243,34244,34245,34246,34247,34248,34249,34250,34251,34252,34253,34254,34255,34256,34257,34258,34259,34260,34261,34262,34263,34264,34265,34266,34267,34268,34269,34270,34271,34272,34273,34274,34275,34276,34277,34278,34279,34280,34281,34282,34283,34284,34285,34286,34287,34288,34289,34290,34291,34292,34293,34294,34295,34296,34297,34298,34299,34300,34301,34302,34303,34304,34305,34306,34307,34308,34309,34310,34311,34312,34313,34314,34315,34316,34317,34318,34319,34320,34321,34322,34323,34324,34325,34326,34327,34328,34329,34330,34331,34332,34333,34334,34335,34336,34337,34338,34339,34340,34341,34342,34343,34344,34345,34346,34347,34348,34349,34350,34351,34352,34353,34354,34355,34356,34357,34358,34359,34360,34361,34362,34363,34364,34365,34366,34367,34368,34369,34370,34371,34372,34373,34374,34375,34376,34377,34378,34379,34380,34381,34382,34383,34384,34385,34386,34387,34388,34389,34390,34391,34392,34393,34394,34395,34396,34397,34398,34399,34400,34401,34402,34403,34404,34405,34406,34407,34408,34409,34410,34411,34412,34413,34414,34415,34416,34417,34418,34419,34420,34421,34422,34423,34424,34425,34426,34427,34428,34429,34430,34431,34432,34433,34434,34435,34436,34437,34438,34439,34440,34441,34442,34443,34444,34445,34446,34447,34448,34449,34450,34451,34452,34453,34454,34455,34456,34457,34458,34459,34460,34461,34462,34463,34464,34465,34466,34467,34468,34469,34470,34471,34472,34473,34474,34475,34476,34477,34478,34479,34480,34481,34482,34483,34484,34485,34486,34487,34488,34489,34490,34491,34492,34493,34494,34495,34496,34497,34498,34499,34500,34501,34502,34503,34504,34505,34506,34507,34508,34509,34510,34511,34512,34513,34514,34515,34516,34517,34518,34519,34520,34521,34522,34523,34524,34525,34526,34527,34528,34529,34530,34531,34532,34533,34534,34535,34536,34537,34538,34539,34540,34541,34542,34543,34544,34545,34546,34547,34548,34549,34550,34551,34552,34553,34554,34555,34556,34557,34558,34559,34560,34561,34562,34563,34564,34565,34566,34567,34568,34569,34570,34571,34572,34573,34574,34575,34576,34577,34578,34579,34580,34581,34582,34583,34584,34585,34586,34587,34588,34589,34590,34591,34592,34593,34594,34595,34596,34597,34598,34599,34600,34601,34602,34603,34604,34605,34606,34607,34608,34609,34610,34611,34612,34613,34614,34615,34616,34617,34618,34619,34620,34621,34622,34623,34624,34625,34626,34627,34628,34629,34630,34631,34632,34633,34634,34635,34636,34637,34638,34639,34640,34641,34642,34643,34644,34645,34646,34647,34648,34649,34650,34651,34652,34653,34654,34655,34656,34657,34658,34659,34660,34661,34662,34663,34664,34665,34666,34667,34668,34669,34670,34671,34672,34673,34674,34675,34676,34677,34678,34679,34680,34681,34682,34683,34684,34685,34686,34687,34688,34689,34690,34691,34692,34693,34694,34695,34696,34697,34698,34699,34700,34701,34702,34703,34704,34705,34706,34707,34708,34709,34710,34711,34712,34713,34714,34715,34716,34717,34718,34719,34720,34721,34722,34723,34724,34725,34726,34727,34728,34729,34730,34731,34732,34733,34734,34735,34736,34737,34738,34739,34740,34741,34742,34743,34744,34745,34746,34747,34748,34749,34750,34751,34752,34753,34754,34755,34756,34757,34758,34759,34760,34761,34762,34763,34764,34765,34766,34767,34768,34769,34770,34771,34772,34773,34774,34775,34776,34777,34778,34779,34780,34781,34782,34783,34784,34785,34786,34787,34788,34789,34790,34791,34792,34793,34794,34795,34796,34797,34798,34799,34800,34801,34802,34803,34804,34805,34806,34807,34808,34809,34810,34811,34812,34813,34814,34815,34816,34817,34818,34819,34820,34821,34822,34823,34824,34825,34826,34827,34828,34829,34830,34831,34832,34833,34834,34835,34836,34837,34838,34839,34840,34841,34842,34843,34844,34845,34846,34847,34848,34849,34850,34851,34852,34853,34854,34855,34856,34857,34858,34859,34860,34861,34862,34863,34864,34865,34866,34867,34868,34869,34870,34871,34872,34873,34874,34875,34876,34877,34878,34879,34880,34881,34882,34883,34884,34885,34886,34887,34888,34889,34890,34891,34892,34893,34894,34895,34896,34897,34898,34899,34900,34901,34902,34903,34904,34905,34906,34907,34908,34909,34910,34911,34912,34913,34914,34915,34916,34917,34918,34919,34920,34921,34922,34923,34924,34925,34926,34927,34928,34929,34930,34931,34932,34933,34934,34935,34936,34937,34938,34939,34940,34941,34942,34943,34944,34945,34946,34947,34948,34949,34950,34951,34952,34953,34954,34955,34956,34957,34958,34959,34960,34961,34962,34963,34964,34965,34966,34967,34968,34969,34970,34971,34972,34973,34974,34975,34976,34977,34978,34979,34980,34981,34982,34983,34984,34985,34986,34987,34988,34989,34990,34991,34992,34993,34994,34995,34996,34997,34998,34999,35000,35001,35002,35003,35004,35005,35006,35007,35008,35009,35010,35011,35012,35013,35014,35015,35016,35017,35018,35019,35020,35021,35022,35023,35024,35025,35026,35027,35028,35029,35030,35031,35032,35033,35034,35035,35036,35037,35038,35039,35040,35041,35042,35043,35044,35045,35046,35047,35048,35049,35050,35051,35052,35053,35054,35055,35056,35057,35058,35059,35060,35061,35062,35063,35064,35065,35066,35067,35068,35069,35070,35071,35072,35073,35074,35075,35076,35077,35078,35079,35080,35081,35082,35083,35084,35085,35086,35087,35088,35089,35090,35091,35092,35093,35094,35095,35096,35097,35098,35099,35100,35101,35102,35103,35104,35105,35106,35107,35108,35109,35110,35111,35112,35113,35114,35115,35116,35117,35118,35119,35120,35121,35122,35123,35124,35125,35126,35127,35128,35129,35130,35131,35132,35133,35134,35135,35136,35137,35138,35139,35140,35141,35142,35143,35144,35145,35146,35147,35148,35149,35150,35151,35152,35153,35154,35155,35156,35157,35158,35159,35160,35161,35162,35163,35164,35165,35166,35167,35168,35169,35170,35171,35172,35173,35174,35175,35176,35177,35178,35179,35180,35181,35182,35183,35184,35185,35186,35187,35188,35189,35190,35191,35192,35193,35194,35195,35196,35197,35198,35199,35200,35201,35202,35203,35204,35205,35206,35207,35208,35209,35210,35211,35212,35213,35214,35215,35216,35217,35218,35219,35220,35221,35222,35223,35224,35225,35226,35227,35228,35229,35230,35231,35232,35233,35234,35235,35236,35237,35238,35239,35240,35241,35242,35243,35244,35245,35246,35247,35248,35249,35250,35251,35252,35253,35254,35255,35256,35257,35258,35259,35260,35261,35262,35263,35264,35265,35266,35267,35268,35269,35270,35271,35272,35273,35274,35275,35276,35277,35278,35279,35280,35281,35282,35283,35284,35285,35286,35287,35288,35289,35290,35291,35292,35293,35294,35295,35296,35297,35298,35299,35300,35301,35302,35303,35304,35305,35306,35307,35308,35309,35310,35311,35312,35313,35314,35315,35316,35317,35318,35319,35320,35321,35322,35323,35324,35325,35326,35327,35328,35329,35330,35331,35332,35333,35334,35335,35336,35337,35338,35339,35340,35341,35342,35343,35344,35345,35346,35347,35348,35349,35350,35351,35352,35353,35354,35355,35356,35357,35358,35359,35360,35361,35362,35363,35364,35365,35366,35367,35368,35369,35370,35371,35372,35373,35374,35375,35376,35377,35378,35379,35380,35381,35382,35383,35384,35385,35386,35387,35388,35389,35390,35391,35392,35393,35394,35395,35396,35397,35398,35399,35400,35401,35402,35403,35404,35405,35406,35407,35408,35409,35410,35411,35412,35413,35414,35415,35416,35417,35418,35419,35420,35421,35422,35423,35424,35425,35426,35427,35428,35429,35430,35431,35432,35433,35434,35435,35436,35437,35438,35439,35440,35441,35442,35443,35444,35445,35446,35447,35448,35449,35450,35451,35452,35453,35454,35455,35456,35457,35458,35459,35460,35461,35462,35463,35464,35465,35466,35467,35468,35469,35470,35471,35472,35473,35474,35475,35476,35477,35478,35479,35480,35481,35482,35483,35484,35485,35486,35487,35488,35489,35490,35491,35492,35493,35494,35495,35496,35497,35498,35499,35500,35501,35502,35503,35504,35505,35506,35507,35508,35509,35510,35511,35512,35513,35514,35515,35516,35517,35518,35519,35520,35521,35522,35523,35524,35525,35526,35527,35528,35529,35530,35531,35532,35533,35534,35535,35536,35537,35538,35539,35540,35541,35542,35543,35544,35545,35546,35547,35548,35549,35550,35551,35552,35553,35554,35555,35556,35557,35558,35559,35560,35561,35562,35563,35564,35565,35566,35567,35568,35569,35570,35571,35572,35573,35574,35575,35576,35577,35578,35579,35580,35581,35582,35583,35584,35585,35586,35587,35588,35589,35590,35591,35592,35593,35594,35595,35596,35597,35598,35599,35600,35601,35602,35603,35604,35605,35606,35607,35608,35609,35610,35611,35612,35613,35614,35615,35616,35617,35618,35619,35620,35621,35622,35623,35624,35625,35626,35627,35628,35629,35630,35631,35632,35633,35634,35635,35636,35637,35638,35639,35640,35641,35642,35643,35644,35645,35646,35647,35648,35649,35650,35651,35652,35653,35654,35655,35656,35657,35658,35659,35660,35661,35662,35663,35664,35665,35666,35667,35668,35669,35670,35671,35672,35673,35674,35675,35676,35677,35678,35679,35680,35681,35682,35683,35684,35685,35686,35687,35688,35689,35690,35691,35692,35693,35694,35695,35696,35697,35698,35699,35700,35701,35702,35703,35704,35705,35706,35707,35708,35709,35710,35711,35712,35713,35714,35715,35716,35717,35718,35719,35720,35721,35722,35723,35724,35725,35726,35727,35728,35729,35730,35731,35732,35733,35734,35735,35736,35737,35738,35739,35740,35741,35742,35743,35744,35745,35746,35747,35748,35749,35750,35751,35752,35753,35754,35755,35756,35757,35758,35759,35760,35761,35762,35763,35764,35765,35766,35767,35768,35769,35770,35771,35772,35773,35774,35775,35776,35777,35778,35779,35780,35781,35782,35783,35784,35785,35786,35787,35788,35789,35790,35791,35792,35793,35794,35795,35796,35797,35798,35799,35800,35801,35802,35803,35804,35805,35806,35807,35808,35809,35810,35811,35812,35813,35814,35815,35816,35817,35818,35819,35820,35821,35822,35823,35824,35825,35826,35827,35828,35829,35830,35831,35832,35833,35834,35835,35836,35837,35838,35839,35840,35841,35842,35843,35844,35845,35846,35847,35848,35849,35850,35851,35852,35853,35854,35855,35856,35857,35858,35859,35860,35861,35862,35863,35864,35865,35866,35867,35868,35869,35870,35871,35872,35873,35874,35875,35876,35877,35878,35879,35880,35881,35882,35883,35884,35885,35886,35887,35888,35889,35890,35891,35892,35893,35894,35895,35896,35897,35898,35899,35900,35901,35902,35903,35904,35905,35906,35907,35908,35909,35910,35911,35912,35913,35914,35915,35916,35917,35918,35919,35920,35921,35922,35923,35924,35925,35926,35927,35928,35929,35930,35931,35932,35933,35934,35935,35936,35937,35938,35939,35940,35941,35942,35943,35944,35945,35946,35947,35948,35949,35950,35951,35952,35953,35954,35955,35956,35957,35958,35959,35960,35961,35962,35963,35964,35965,35966,35967,35968,35969,35970,35971,35972,35973,35974,35975,35976,35977,35978,35979,35980,35981,35982,35983,35984,35985,35986,35987,35988,35989,35990,35991,35992,35993,35994,35995,35996,35997,35998,35999,36000,36001,36002,36003,36004,36005,36006,36007,36008,36009,36010,36011,36012,36013,36014,36015,36016,36017,36018,36019,36020,36021,36022,36023,36024,36025,36026,36027,36028,36029,36030,36031,36032,36033,36034,36035,36036,36037,36038,36039,36040,36041,36042,36043,36044,36045,36046,36047,36048,36049,36050,36051,36052,36053,36054,36055,36056,36057,36058,36059,36060,36061,36062,36063,36064,36065,36066,36067,36068,36069,36070,36071,36072,36073,36074,36075,36076,36077,36078,36079,36080,36081,36082,36083,36084,36085,36086,36087,36088,36089,36090,36091,36092,36093,36094,36095,36096,36097,36098,36099,36100,36101,36102,36103,36104,36105,36106,36107,36108,36109,36110,36111,36112,36113,36114,36115,36116,36117,36118,36119,36120,36121,36122,36123,36124,36125,36126,36127,36128,36129,36130,36131,36132,36133,36134,36135,36136,36137,36138,36139,36140,36141,36142,36143,36144,36145,36146,36147,36148,36149,36150,36151,36152,36153,36154,36155,36156,36157,36158,36159,36160,36161,36162,36163,36164,36165,36166,36167,36168,36169,36170,36171,36172,36173,36174,36175,36176,36177,36178,36179,36180,36181,36182,36183,36184,36185,36186,36187,36188,36189,36190,36191,36192,36193,36194,36195,36196,36197,36198,36199,36200,36201,36202,36203,36204,36205,36206,36207,36208,36209,36210,36211,36212,36213,36214,36215,36216,36217,36218,36219,36220,36221,36222,36223,36224,36225,36226,36227,36228,36229,36230,36231,36232,36233,36234,36235,36236,36237,36238,36239,36240,36241,36242,36243,36244,36245,36246,36247,36248,36249,36250,36251,36252,36253,36254,36255,36256,36257,36258,36259,36260,36261,36262,36263,36264,36265,36266,36267,36268,36269,36270,36271,36272,36273,36274,36275,36276,36277,36278,36279,36280,36281,36282,36283,36284,36285,36286,36287,36288,36289,36290,36291,36292,36293,36294,36295,36296,36297,36298,36299,36300,36301,36302,36303,36304,36305,36306,36307,36308,36309,36310,36311,36312,36313,36314,36315,36316,36317,36318,36319,36320,36321,36322,36323,36324,36325,36326,36327,36328,36329,36330,36331,36332,36333,36334,36335,36336,36337,36338,36339,36340,36341,36342,36343,36344,36345,36346,36347,36348,36349,36350,36351,36352,36353,36354,36355,36356,36357,36358,36359,36360,36361,36362,36363,36364,36365,36366,36367,36368,36369,36370,36371,36372,36373,36374,36375,36376,36377,36378,36379,36380,36381,36382,36383,36384,36385,36386,36387,36388,36389,36390,36391,36392,36393,36394,36395,36396,36397,36398,36399,36400,36401,36402,36403,36404,36405,36406,36407,36408,36409,36410,36411,36412,36413,36414,36415,36416,36417,36418,36419,36420,36421,36422,36423,36424,36425,36426,36427,36428,36429,36430,36431,36432,36433,36434,36435,36436,36437,36438,36439,36440,36441,36442,36443,36444,36445,36446,36447,36448,36449,36450,36451,36452,36453,36454,36455,36456,36457,36458,36459,36460,36461,36462,36463,36464,36465,36466,36467,36468,36469,36470,36471,36472,36473,36474,36475,36476,36477,36478,36479,36480,36481,36482,36483,36484,36485,36486,36487,36488,36489,36490,36491,36492,36493,36494,36495,36496,36497,36498,36499,36500,36501,36502,36503,36504,36505,36506,36507,36508,36509,36510,36511,36512,36513,36514,36515,36516,36517,36518,36519,36520,36521,36522,36523,36524,36525,36526,36527,36528,36529,36530,36531,36532,36533,36534,36535,36536,36537,36538,36539,36540,36541,36542,36543,36544,36545,36546,36547,36548,36549,36550,36551,36552,36553,36554,36555,36556,36557,36558,36559,36560,36561,36562,36563,36564,36565,36566,36567,36568,36569,36570,36571,36572,36573,36574,36575,36576,36577,36578,36579,36580,36581,36582,36583,36584,36585,36586,36587,36588,36589,36590,36591,36592,36593,36594,36595,36596,36597,36598,36599,36600,36601,36602,36603,36604,36605,36606,36607,36608,36609,36610,36611,36612,36613,36614,36615,36616,36617,36618,36619,36620,36621,36622,36623,36624,36625,36626,36627,36628,36629,36630,36631,36632,36633,36634,36635,36636,36637,36638,36639,36640,36641,36642,36643,36644,36645,36646,36647,36648,36649,36650,36651,36652,36653,36654,36655,36656,36657,36658,36659,36660,36661,36662,36663,36664,36665,36666,36667,36668,36669,36670,36671,36672,36673,36674,36675,36676,36677,36678,36679,36680,36681,36682,36683,36684,36685,36686,36687,36688,36689,36690,36691,36692,36693,36694,36695,36696,36697,36698,36699,36700,36701,36702,36703,36704,36705,36706,36707,36708,36709,36710,36711,36712,36713,36714,36715,36716,36717,36718,36719,36720,36721,36722,36723,36724,36725,36726,36727,36728,36729,36730,36731,36732,36733,36734,36735,36736,36737,36738,36739,36740,36741,36742,36743,36744,36745,36746,36747,36748,36749,36750,36751,36752,36753,36754,36755,36756,36757,36758,36759,36760,36761,36762,36763,36764,36765,36766,36767,36768,36769,36770,36771,36772,36773,36774,36775,36776,36777,36778,36779,36780,36781,36782,36783,36784,36785,36786,36787,36788,36789,36790,36791,36792,36793,36794,36795,36796,36797,36798,36799,36800,36801,36802,36803,36804,36805,36806,36807,36808,36809,36810,36811,36812,36813,36814,36815,36816,36817,36818,36819,36820,36821,36822,36823,36824,36825,36826,36827,36828,36829,36830,36831,36832,36833,36834,36835,36836,36837,36838,36839,36840,36841,36842,36843,36844,36845,36846,36847,36848,36849,36850,36851,36852,36853,36854,36855,36856,36857,36858,36859,36860,36861,36862,36863,36864,36865,36866,36867,36868,36869,36870,36871,36872,36873,36874,36875,36876,36877,36878,36879,36880,36881,36882,36883,36884,36885,36886,36887,36888,36889,36890,36891,36892,36893,36894,36895,36896,36897,36898,36899,36900,36901,36902,36903,36904,36905,36906,36907,36908,36909,36910,36911,36912,36913,36914,36915,36916,36917,36918,36919,36920,36921,36922,36923,36924,36925,36926,36927,36928,36929,36930,36931,36932,36933,36934,36935,36936,36937,36938,36939,36940,36941,36942,36943,36944,36945,36946,36947,36948,36949,36950,36951,36952,36953,36954,36955,36956,36957,36958,36959,36960,36961,36962,36963,36964,36965,36966,36967,36968,36969,36970,36971,36972,36973,36974,36975,36976,36977,36978,36979,36980,36981,36982,36983,36984,36985,36986,36987,36988,36989,36990,36991,36992,36993,36994,36995,36996,36997,36998,36999,37000,37001,37002,37003,37004,37005,37006,37007,37008,37009,37010,37011,37012,37013,37014,37015,37016,37017,37018,37019,37020,37021,37022,37023,37024,37025,37026,37027,37028,37029,37030,37031,37032,37033,37034,37035,37036,37037,37038,37039,37040,37041,37042,37043,37044,37045,37046,37047,37048,37049,37050,37051,37052,37053,37054,37055,37056,37057,37058,37059,37060,37061,37062,37063,37064,37065,37066,37067,37068,37069,37070,37071,37072,37073,37074,37075,37076,37077,37078,37079,37080,37081,37082,37083,37084,37085,37086,37087,37088,37089,37090,37091,37092,37093,37094,37095,37096,37097,37098,37099,37100,37101,37102,37103,37104,37105,37106,37107,37108,37109,37110,37111,37112,37113,37114,37115,37116,37117,37118,37119,37120,37121,37122,37123,37124,37125,37126,37127,37128,37129,37130,37131,37132,37133,37134,37135,37136,37137,37138,37139,37140,37141,37142,37143,37144,37145,37146,37147,37148,37149,37150,37151,37152,37153,37154,37155,37156,37157,37158,37159,37160,37161,37162,37163,37164,37165,37166,37167,37168,37169,37170,37171,37172,37173,37174,37175,37176,37177,37178,37179,37180,37181,37182,37183,37184,37185,37186,37187,37188,37189,37190,37191,37192,37193,37194,37195,37196,37197,37198,37199,37200,37201,37202,37203,37204,37205,37206,37207,37208,37209,37210,37211,37212,37213,37214,37215,37216,37217,37218,37219,37220,37221,37222,37223,37224,37225,37226,37227,37228,37229,37230,37231,37232,37233,37234,37235,37236,37237,37238,37239,37240,37241,37242,37243,37244,37245,37246,37247,37248,37249,37250,37251,37252,37253,37254,37255,37256,37257,37258,37259,37260,37261,37262,37263,37264,37265,37266,37267,37268,37269,37270,37271,37272,37273,37274,37275,37276,37277,37278,37279,37280,37281,37282,37283,37284,37285,37286,37287,37288,37289,37290,37291,37292,37293,37294,37295,37296,37297,37298,37299,37300,37301,37302,37303,37304,37305,37306,37307,37308,37309,37310,37311,37312,37313,37314,37315,37316,37317,37318,37319,37320,37321,37322,37323,37324,37325,37326,37327,37328,37329,37330,37331,37332,37333,37334,37335,37336,37337,37338,37339,37340,37341,37342,37343,37344,37345,37346,37347,37348,37349,37350,37351,37352,37353,37354,37355,37356,37357,37358,37359,37360,37361,37362,37363,37364,37365,37366,37367,37368,37369,37370,37371,37372,37373,37374,37375,37376,37377,37378,37379,37380,37381,37382,37383,37384,37385,37386,37387,37388,37389,37390,37391,37392,37393,37394,37395,37396,37397,37398,37399,37400,37401,37402,37403,37404,37405,37406,37407,37408,37409,37410,37411,37412,37413,37414,37415,37416,37417,37418,37419,37420,37421,37422,37423,37424,37425,37426,37427,37428,37429,37430,37431,37432,37433,37434,37435,37436,37437,37438,37439,37440,37441,37442,37443,37444,37445,37446,37447,37448,37449,37450,37451,37452,37453,37454,37455,37456,37457,37458,37459,37460,37461,37462,37463,37464,37465,37466,37467,37468,37469,37470,37471,37472,37473,37474,37475,37476,37477,37478,37479,37480,37481,37482,37483,37484,37485,37486,37487,37488,37489,37490,37491,37492,37493,37494,37495,37496,37497,37498,37499,37500,37501,37502,37503,37504,37505,37506,37507,37508,37509,37510,37511,37512,37513,37514,37515,37516,37517,37518,37519,37520,37521,37522,37523,37524,37525,37526,37527,37528,37529,37530,37531,37532,37533,37534,37535,37536,37537,37538,37539,37540,37541,37542,37543,37544,37545,37546,37547,37548,37549,37550,37551,37552,37553,37554,37555,37556,37557,37558,37559,37560,37561,37562,37563,37564,37565,37566,37567,37568,37569,37570,37571,37572,37573,37574,37575,37576,37577,37578,37579,37580,37581,37582,37583,37584,37585,37586,37587,37588,37589,37590,37591,37592,37593,37594,37595,37596,37597,37598,37599,37600,37601,37602,37603,37604,37605,37606,37607,37608,37609,37610,37611,37612,37613,37614,37615,37616,37617,37618,37619,37620,37621,37622,37623,37624,37625,37626,37627,37628,37629,37630,37631,37632,37633,37634,37635,37636,37637,37638,37639,37640,37641,37642,37643,37644,37645,37646,37647,37648,37649,37650,37651,37652,37653,37654,37655,37656,37657,37658,37659,37660,37661,37662,37663,37664,37665,37666,37667,37668,37669,37670,37671,37672,37673,37674,37675,37676,37677,37678,37679,37680,37681,37682,37683,37684,37685,37686,37687,37688,37689,37690,37691,37692,37693,37694,37695,37696,37697,37698,37699,37700,37701,37702,37703,37704,37705,37706,37707,37708,37709,37710,37711,37712,37713,37714,37715,37716,37717,37718,37719,37720,37721,37722,37723,37724,37725,37726,37727,37728,37729,37730,37731,37732,37733,37734,37735,37736,37737,37738,37739,37740,37741,37742,37743,37744,37745,37746,37747,37748,37749,37750,37751,37752,37753,37754,37755,37756,37757,37758,37759,37760,37761,37762,37763,37764,37765,37766,37767,37768,37769,37770,37771,37772,37773,37774,37775,37776,37777,37778,37779,37780,37781,37782,37783,37784,37785,37786,37787,37788,37789,37790,37791,37792,37793,37794,37795,37796,37797,37798,37799,37800,37801,37802,37803,37804,37805,37806,37807,37808,37809,37810,37811,37812,37813,37814,37815,37816,37817,37818,37819,37820,37821,37822,37823,37824,37825,37826,37827,37828,37829,37830,37831,37832,37833,37834,37835,37836,37837,37838,37839,37840,37841,37842,37843,37844,37845,37846,37847,37848,37849,37850,37851,37852,37853,37854,37855,37856,37857,37858,37859,37860,37861,37862,37863,37864,37865,37866,37867,37868,37869,37870,37871,37872,37873,37874,37875,37876,37877,37878,37879,37880,37881,37882,37883,37884,37885,37886,37887,37888,37889,37890,37891,37892,37893,37894,37895,37896,37897,37898,37899,37900,37901,37902,37903,37904,37905,37906,37907,37908,37909,37910,37911,37912,37913,37914,37915,37916,37917,37918,37919,37920,37921,37922,37923,37924,37925,37926,37927,37928,37929,37930,37931,37932,37933,37934,37935,37936,37937,37938,37939,37940,37941,37942,37943,37944,37945,37946,37947,37948,37949,37950,37951,37952,37953,37954,37955,37956,37957,37958,37959,37960,37961,37962,37963,37964,37965,37966,37967,37968,37969,37970,37971,37972,37973,37974,37975,37976,37977,37978,37979,37980,37981,37982,37983,37984,37985,37986,37987,37988,37989,37990,37991,37992,37993,37994,37995,37996,37997,37998,37999,38000,38001,38002,38003,38004,38005,38006,38007,38008,38009,38010,38011,38012,38013,38014,38015,38016,38017,38018,38019,38020,38021,38022,38023,38024,38025,38026,38027,38028,38029,38030,38031,38032,38033,38034,38035,38036,38037,38038,38039,38040,38041,38042,38043,38044,38045,38046,38047,38048,38049,38050,38051,38052,38053,38054,38055,38056,38057,38058,38059,38060,38061,38062,38063,38064,38065,38066,38067,38068,38069,38070,38071,38072,38073,38074,38075,38076,38077,38078,38079,38080,38081,38082,38083,38084,38085,38086,38087,38088,38089,38090,38091,38092,38093,38094,38095,38096,38097,38098,38099,38100,38101,38102,38103,38104,38105,38106,38107,38108,38109,38110,38111,38112,38113,38114,38115,38116,38117,38118,38119,38120,38121,38122,38123,38124,38125,38126,38127,38128,38129,38130,38131,38132,38133,38134,38135,38136,38137,38138,38139,38140,38141,38142,38143,38144,38145,38146,38147,38148,38149,38150,38151,38152,38153,38154,38155,38156,38157,38158,38159,38160,38161,38162,38163,38164,38165,38166,38167,38168,38169,38170,38171,38172,38173,38174,38175,38176,38177,38178,38179,38180,38181,38182,38183,38184,38185,38186,38187,38188,38189,38190,38191,38192,38193,38194,38195,38196,38197,38198,38199,38200,38201,38202,38203,38204,38205,38206,38207,38208,38209,38210,38211,38212,38213,38214,38215,38216,38217,38218,38219,38220,38221,38222,38223,38224,38225,38226,38227,38228,38229,38230,38231,38232,38233,38234,38235,38236,38237,38238,38239,38240,38241,38242,38243,38244,38245,38246,38247,38248,38249,38250,38251,38252,38253,38254,38255,38256,38257,38258,38259,38260,38261,38262,38263,38264,38265,38266,38267,38268,38269,38270,38271,38272,38273,38274,38275,38276,38277,38278,38279,38280,38281,38282,38283,38284,38285,38286,38287,38288,38289,38290,38291,38292,38293,38294,38295,38296,38297,38298,38299,38300,38301,38302,38303,38304,38305,38306,38307,38308,38309,38310,38311,38312,38313,38314,38315,38316,38317,38318,38319,38320,38321,38322,38323,38324,38325,38326,38327,38328,38329,38330,38331,38332,38333,38334,38335,38336,38337,38338,38339,38340,38341,38342,38343,38344,38345,38346,38347,38348,38349,38350,38351,38352,38353,38354,38355,38356,38357,38358,38359,38360,38361,38362,38363,38364,38365,38366,38367,38368,38369,38370,38371,38372,38373,38374,38375,38376,38377,38378,38379,38380,38381,38382,38383,38384,38385,38386,38387,38388,38389,38390,38391,38392,38393,38394,38395,38396,38397,38398,38399,38400,38401,38402,38403,38404,38405,38406,38407,38408,38409,38410,38411,38412,38413,38414,38415,38416,38417,38418,38419,38420,38421,38422,38423,38424,38425,38426,38427,38428,38429,38430,38431,38432,38433,38434,38435,38436,38437,38438,38439,38440,38441,38442,38443,38444,38445,38446,38447,38448,38449,38450,38451,38452,38453,38454,38455,38456,38457,38458,38459,38460,38461,38462,38463,38464,38465,38466,38467,38468,38469,38470,38471,38472,38473,38474,38475,38476,38477,38478,38479,38480,38481,38482,38483,38484,38485,38486,38487,38488,38489,38490,38491,38492,38493,38494,38495,38496,38497,38498,38499,38500,38501,38502,38503,38504,38505,38506,38507,38508,38509,38510,38511,38512,38513,38514,38515,38516,38517,38518,38519,38520,38521,38522,38523,38524,38525,38526,38527,38528,38529,38530,38531,38532,38533,38534,38535,38536,38537,38538,38539,38540,38541,38542,38543,38544,38545,38546,38547,38548,38549,38550,38551,38552,38553,38554,38555,38556,38557,38558,38559,38560,38561,38562,38563,38564,38565,38566,38567,38568,38569,38570,38571,38572,38573,38574,38575,38576,38577,38578,38579,38580,38581,38582,38583,38584,38585,38586,38587,38588,38589,38590,38591,38592,38593,38594,38595,38596,38597,38598,38599,38600,38601,38602,38603,38604,38605,38606,38607,38608,38609,38610,38611,38612,38613,38614,38615,38616,38617,38618,38619,38620,38621,38622,38623,38624,38625,38626,38627,38628,38629,38630,38631,38632,38633,38634,38635,38636,38637,38638,38639,38640,38641,38642,38643,38644,38645,38646,38647,38648,38649,38650,38651,38652,38653,38654,38655,38656,38657,38658,38659,38660,38661,38662,38663,38664,38665,38666,38667,38668,38669,38670,38671,38672,38673,38674,38675,38676,38677,38678,38679,38680,38681,38682,38683,38684,38685,38686,38687,38688,38689,38690,38691,38692,38693,38694,38695,38696,38697,38698,38699,38700,38701,38702,38703,38704,38705,38706,38707,38708,38709,38710,38711,38712,38713,38714,38715,38716,38717,38718,38719,38720,38721,38722,38723,38724,38725,38726,38727,38728,38729,38730,38731,38732,38733,38734,38735,38736,38737,38738,38739,38740,38741,38742,38743,38744,38745,38746,38747,38748,38749,38750,38751,38752,38753,38754,38755,38756,38757,38758,38759,38760,38761,38762,38763,38764,38765,38766,38767,38768,38769,38770,38771,38772,38773,38774,38775,38776,38777,38778,38779,38780,38781,38782,38783,38784,38785,38786,38787,38788,38789,38790,38791,38792,38793,38794,38795,38796,38797,38798,38799,38800,38801,38802,38803,38804,38805,38806,38807,38808,38809,38810,38811,38812,38813,38814,38815,38816,38817,38818,38819,38820,38821,38822,38823,38824,38825,38826,38827,38828,38829,38830,38831,38832,38833,38834,38835,38836,38837,38838,38839,38840,38841,38842,38843,38844,38845,38846,38847,38848,38849,38850,38851,38852,38853,38854,38855,38856,38857,38858,38859,38860,38861,38862,38863,38864,38865,38866,38867,38868,38869,38870,38871,38872,38873,38874,38875,38876,38877,38878,38879,38880,38881,38882,38883,38884,38885,38886,38887,38888,38889,38890,38891,38892,38893,38894,38895,38896,38897,38898,38899,38900,38901,38902,38903,38904,38905,38906,38907,38908,38909,38910,38911,38912,38913,38914,38915,38916,38917,38918,38919,38920,38921,38922,38923,38924,38925,38926,38927,38928,38929,38930,38931,38932,38933,38934,38935,38936,38937,38938,38939,38940,38941,38942,38943,38944,38945,38946,38947,38948,38949,38950,38951,38952,38953,38954,38955,38956,38957,38958,38959,38960,38961,38962,38963,38964,38965,38966,38967,38968,38969,38970,38971,38972,38973,38974,38975,38976,38977,38978,38979,38980,38981,38982,38983,38984,38985,38986,38987,38988,38989,38990,38991,38992,38993,38994,38995,38996,38997,38998,38999,39000,39001,39002,39003,39004,39005,39006,39007,39008,39009,39010,39011,39012,39013,39014,39015,39016,39017,39018,39019,39020,39021,39022,39023,39024,39025,39026,39027,39028,39029,39030,39031,39032,39033,39034,39035,39036,39037,39038,39039,39040,39041,39042,39043,39044,39045,39046,39047,39048,39049,39050,39051,39052,39053,39054,39055,39056,39057,39058,39059,39060,39061,39062,39063,39064,39065,39066,39067,39068,39069,39070,39071,39072,39073,39074,39075,39076,39077,39078,39079,39080,39081,39082,39083,39084,39085,39086,39087,39088,39089,39090,39091,39092,39093,39094,39095,39096,39097,39098,39099,39100,39101,39102,39103,39104,39105,39106,39107,39108,39109,39110,39111,39112,39113,39114,39115,39116,39117,39118,39119,39120,39121,39122,39123,39124,39125,39126,39127,39128,39129,39130,39131,39132,39133,39134,39135,39136,39137,39138,39139,39140,39141,39142,39143,39144,39145,39146,39147,39148,39149,39150,39151,39152,39153,39154,39155,39156,39157,39158,39159,39160,39161,39162,39163,39164,39165,39166,39167,39168,39169,39170,39171,39172,39173,39174,39175,39176,39177,39178,39179,39180,39181,39182,39183,39184,39185,39186,39187,39188,39189,39190,39191,39192,39193,39194,39195,39196,39197,39198,39199,39200,39201,39202,39203,39204,39205,39206,39207,39208,39209,39210,39211,39212,39213,39214,39215,39216,39217,39218,39219,39220,39221,39222,39223,39224,39225,39226,39227,39228,39229,39230,39231,39232,39233,39234,39235,39236,39237,39238,39239,39240,39241,39242,39243,39244,39245,39246,39247,39248,39249,39250,39251,39252,39253,39254,39255,39256,39257,39258,39259,39260,39261,39262,39263,39264,39265,39266,39267,39268,39269,39270,39271,39272,39273,39274,39275,39276,39277,39278,39279,39280,39281,39282,39283,39284,39285,39286,39287,39288,39289,39290,39291,39292,39293,39294,39295,39296,39297,39298,39299,39300,39301,39302,39303,39304,39305,39306,39307,39308,39309,39310,39311,39312,39313,39314,39315,39316,39317,39318,39319,39320,39321,39322,39323,39324,39325,39326,39327,39328,39329,39330,39331,39332,39333,39334,39335,39336,39337,39338,39339,39340,39341,39342,39343,39344,39345,39346,39347,39348,39349,39350,39351,39352,39353,39354,39355,39356,39357,39358,39359,39360,39361,39362,39363,39364,39365,39366,39367,39368,39369,39370,39371,39372,39373,39374,39375,39376,39377,39378,39379,39380,39381,39382,39383,39384,39385,39386,39387,39388,39389,39390,39391,39392,39393,39394,39395,39396,39397,39398,39399,39400,39401,39402,39403,39404,39405,39406,39407,39408,39409,39410,39411,39412,39413,39414,39415,39416,39417,39418,39419,39420,39421,39422,39423,39424,39425,39426,39427,39428,39429,39430,39431,39432,39433,39434,39435,39436,39437,39438,39439,39440,39441,39442,39443,39444,39445,39446,39447,39448,39449,39450,39451,39452,39453,39454,39455,39456,39457,39458,39459,39460,39461,39462,39463,39464,39465,39466,39467,39468,39469,39470,39471,39472,39473,39474,39475,39476,39477,39478,39479,39480,39481,39482,39483,39484,39485,39486,39487,39488,39489,39490,39491,39492,39493,39494,39495,39496,39497,39498,39499,39500,39501,39502,39503,39504,39505,39506,39507,39508,39509,39510,39511,39512,39513,39514,39515,39516,39517,39518,39519,39520,39521,39522,39523,39524,39525,39526,39527,39528,39529,39530,39531,39532,39533,39534,39535,39536,39537,39538,39539,39540,39541,39542,39543,39544,39545,39546,39547,39548,39549,39550,39551,39552,39553,39554,39555,39556,39557,39558,39559,39560,39561,39562,39563,39564,39565,39566,39567,39568,39569,39570,39571,39572,39573,39574,39575,39576,39577,39578,39579,39580,39581,39582,39583,39584,39585,39586,39587,39588,39589,39590,39591,39592,39593,39594,39595,39596,39597,39598,39599,39600,39601,39602,39603,39604,39605,39606,39607,39608,39609,39610,39611,39612,39613,39614,39615,39616,39617,39618,39619,39620,39621,39622,39623,39624,39625,39626,39627,39628,39629,39630,39631,39632,39633,39634,39635,39636,39637,39638,39639,39640,39641,39642,39643,39644,39645,39646,39647,39648,39649,39650,39651,39652,39653,39654,39655,39656,39657,39658,39659,39660,39661,39662,39663,39664,39665,39666,39667,39668,39669,39670,39671,39672,39673,39674,39675,39676,39677,39678,39679,39680,39681,39682,39683,39684,39685,39686,39687,39688,39689,39690,39691,39692,39693,39694,39695,39696,39697,39698,39699,39700,39701,39702,39703,39704,39705,39706,39707,39708,39709,39710,39711,39712,39713,39714,39715,39716,39717,39718,39719,39720,39721,39722,39723,39724,39725,39726,39727,39728,39729,39730,39731,39732,39733,39734,39735,39736,39737,39738,39739,39740,39741,39742,39743,39744,39745,39746,39747,39748,39749,39750,39751,39752,39753,39754,39755,39756,39757,39758,39759,39760,39761,39762,39763,39764,39765,39766,39767,39768,39769,39770,39771,39772,39773,39774,39775,39776,39777,39778,39779,39780,39781,39782,39783,39784,39785,39786,39787,39788,39789,39790,39791,39792,39793,39794,39795,39796,39797,39798,39799,39800,39801,39802,39803,39804,39805,39806,39807,39808,39809,39810,39811,39812,39813,39814,39815,39816,39817,39818,39819,39820,39821,39822,39823,39824,39825,39826,39827,39828,39829,39830,39831,39832,39833,39834,39835,39836,39837,39838,39839,39840,39841,39842,39843,39844,39845,39846,39847,39848,39849,39850,39851,39852,39853,39854,39855,39856,39857,39858,39859,39860,39861,39862,39863,39864,39865,39866,39867,39868,39869,39870,39871,39872,39873,39874,39875,39876,39877,39878,39879,39880,39881,39882,39883,39884,39885,39886,39887,39888,39889,39890,39891,39892,39893,39894,39895,39896,39897,39898,39899,39900,39901,39902,39903,39904,39905,39906,39907,39908,39909,39910,39911,39912,39913,39914,39915,39916,39917,39918,39919,39920,39921,39922,39923,39924,39925,39926,39927,39928,39929,39930,39931,39932,39933,39934,39935,39936,39937,39938,39939,39940,39941,39942,39943,39944,39945,39946,39947,39948,39949,39950,39951,39952,39953,39954,39955,39956,39957,39958,39959,39960,39961,39962,39963,39964,39965,39966,39967,39968,39969,39970,39971,39972,39973,39974,39975,39976,39977,39978,39979,39980,39981,39982,39983,39984,39985,39986,39987,39988,39989,39990,39991,39992,39993,39994,39995,39996,39997,39998,39999,40000,40001,40002,40003,40004,40005,40006,40007,40008,40009,40010,40011,40012,40013,40014,40015,40016,40017,40018,40019,40020,40021,40022,40023,40024,40025,40026,40027,40028,40029,40030,40031,40032,40033,40034,40035,40036,40037,40038,40039,40040,40041,40042,40043,40044,40045,40046,40047,40048,40049,40050,40051,40052,40053,40054,40055,40056,40057,40058,40059,40060,40061,40062,40063,40064,40065,40066,40067,40068,40069,40070,40071,40072,40073,40074,40075,40076,40077,40078,40079,40080,40081,40082,40083,40084,40085,40086,40087,40088,40089,40090,40091,40092,40093,40094,40095,40096,40097,40098,40099,40100,40101,40102,40103,40104,40105,40106,40107,40108,40109,40110,40111,40112,40113,40114,40115,40116,40117,40118,40119,40120,40121,40122,40123,40124,40125,40126,40127,40128,40129,40130,40131,40132,40133,40134,40135,40136,40137,40138,40139,40140,40141,40142,40143,40144,40145,40146,40147,40148,40149,40150,40151,40152,40153,40154,40155,40156,40157,40158,40159,40160,40161,40162,40163,40164,40165,40166,40167,40168,40169,40170,40171,40172,40173,40174,40175,40176,40177,40178,40179,40180,40181,40182,40183,40184,40185,40186,40187,40188,40189,40190,40191,40192,40193,40194,40195,40196,40197,40198,40199,40200,40201,40202,40203,40204,40205,40206,40207,40208,40209,40210,40211,40212,40213,40214,40215,40216,40217,40218,40219,40220,40221,40222,40223,40224,40225,40226,40227,40228,40229,40230,40231,40232,40233,40234,40235,40236,40237,40238,40239,40240,40241,40242,40243,40244,40245,40246,40247,40248,40249,40250,40251,40252,40253,40254,40255,40256,40257,40258,40259,40260,40261,40262,40263,40264,40265,40266,40267,40268,40269,40270,40271,40272,40273,40274,40275,40276,40277,40278,40279,40280,40281,40282,40283,40284,40285,40286,40287,40288,40289,40290,40291,40292,40293,40294,40295,40296,40297,40298,40299,40300,40301,40302,40303,40304,40305,40306,40307,40308,40309,40310,40311,40312,40313,40314,40315,40316,40317,40318,40319,40320,40321,40322,40323,40324,40325,40326,40327,40328,40329,40330,40331,40332,40333,40334,40335,40336,40337,40338,40339,40340,40341,40342,40343,40344,40345,40346,40347,40348,40349,40350,40351,40352,40353,40354,40355,40356,40357,40358,40359,40360,40361,40362,40363,40364,40365,40366,40367,40368,40369,40370,40371,40372,40373,40374,40375,40376,40377,40378,40379,40380,40381,40382,40383,40384,40385,40386,40387,40388,40389,40390,40391,40392,40393,40394,40395,40396,40397,40398,40399,40400,40401,40402,40403,40404,40405,40406,40407,40408,40409,40410,40411,40412,40413,40414,40415,40416,40417,40418,40419,40420,40421,40422,40423,40424,40425,40426,40427,40428,40429,40430,40431,40432,40433,40434,40435,40436,40437,40438,40439,40440,40441,40442,40443,40444,40445,40446,40447,40448,40449,40450,40451,40452,40453,40454,40455,40456,40457,40458,40459,40460,40461,40462,40463,40464,40465,40466,40467,40468,40469,40470,40471,40472,40473,40474,40475,40476,40477,40478,40479,40480,40481,40482,40483,40484,40485,40486,40487,40488,40489,40490,40491,40492,40493,40494,40495,40496,40497,40498,40499,40500,40501,40502,40503,40504,40505,40506,40507,40508,40509,40510,40511,40512,40513,40514,40515,40516,40517,40518,40519,40520,40521,40522,40523,40524,40525,40526,40527,40528,40529,40530,40531,40532,40533,40534,40535,40536,40537,40538,40539,40540,40541,40542,40543,40544,40545,40546,40547,40548,40549,40550,40551,40552,40553,40554,40555,40556,40557,40558,40559,40560,40561,40562,40563,40564,40565,40566,40567,40568,40569,40570,40571,40572,40573,40574,40575,40576,40577,40578,40579,40580,40581,40582,40583,40584,40585,40586,40587,40588,40589,40590,40591,40592,40593,40594,40595,40596,40597,40598,40599,40600,40601,40602,40603,40604,40605,40606,40607,40608,40609,40610,40611,40612,40613,40614,40615,40616,40617,40618,40619,40620,40621,40622,40623,40624,40625,40626,40627,40628,40629,40630,40631,40632,40633,40634,40635,40636,40637,40638,40639,40640,40641,40642,40643,40644,40645,40646,40647,40648,40649,40650,40651,40652,40653,40654,40655,40656,40657,40658,40659,40660,40661,40662,40663,40664,40665,40666,40667,40668,40669,40670,40671,40672,40673,40674,40675,40676,40677,40678,40679,40680,40681,40682,40683,40684,40685,40686,40687,40688,40689,40690,40691,40692,40693,40694,40695,40696,40697,40698,40699,40700,40701,40702,40703,40704,40705,40706,40707,40708,40709,40710,40711,40712,40713,40714,40715,40716,40717,40718,40719,40720,40721,40722,40723,40724,40725,40726,40727,40728,40729,40730,40731,40732,40733,40734,40735,40736,40737,40738,40739,40740,40741,40742,40743,40744,40745,40746,40747,40748,40749,40750,40751,40752,40753,40754,40755,40756,40757,40758,40759,40760,40761,40762,40763,40764,40765,40766,40767,40768,40769,40770,40771,40772,40773,40774,40775,40776,40777,40778,40779,40780,40781,40782,40783,40784,40785,40786,40787,40788,40789,40790,40791,40792,40793,40794,40795,40796,40797,40798,40799,40800,40801,40802,40803,40804,40805,40806,40807,40808,40809,40810,40811,40812,40813,40814,40815,40816,40817,40818,40819,40820,40821,40822,40823,40824,40825,40826,40827,40828,40829,40830,40831,40832,40833,40834,40835,40836,40837,40838,40839,40840,40841,40842,40843,40844,40845,40846,40847,40848,40849,40850,40851,40852,40853,40854,40855,40856,40857,40858,40859,40860,40861,40862,40863,40864,40865,40866,40867,40868,40869,40870,40871,40872,40873,40874,40875,40876,40877,40878,40879,40880,40881,40882,40883,40884,40885,40886,40887,40888,40889,40890,40891,40892,40893,40894,40895,40896,40897,40898,40899,40900,40901,40902,40903,40904,40905,40906,40907,40908,40909,40910,40911,40912,40913,40914,40915,40916,40917,40918,40919,40920,40921,40922,40923,40924,40925,40926,40927,40928,40929,40930,40931,40932,40933,40934,40935,40936,40937,40938,40939,40940,40941,40942,40943,40960,40961,40962,40963,40964,40965,40966,40967,40968,40969,40970,40971,40972,40973,40974,40975,40976,40977,40978,40979,40980,40981,40982,40983,40984,40985,40986,40987,40988,40989,40990,40991,40992,40993,40994,40995,40996,40997,40998,40999,41000,41001,41002,41003,41004,41005,41006,41007,41008,41009,41010,41011,41012,41013,41014,41015,41016,41017,41018,41019,41020,41021,41022,41023,41024,41025,41026,41027,41028,41029,41030,41031,41032,41033,41034,41035,41036,41037,41038,41039,41040,41041,41042,41043,41044,41045,41046,41047,41048,41049,41050,41051,41052,41053,41054,41055,41056,41057,41058,41059,41060,41061,41062,41063,41064,41065,41066,41067,41068,41069,41070,41071,41072,41073,41074,41075,41076,41077,41078,41079,41080,41081,41082,41083,41084,41085,41086,41087,41088,41089,41090,41091,41092,41093,41094,41095,41096,41097,41098,41099,41100,41101,41102,41103,41104,41105,41106,41107,41108,41109,41110,41111,41112,41113,41114,41115,41116,41117,41118,41119,41120,41121,41122,41123,41124,41125,41126,41127,41128,41129,41130,41131,41132,41133,41134,41135,41136,41137,41138,41139,41140,41141,41142,41143,41144,41145,41146,41147,41148,41149,41150,41151,41152,41153,41154,41155,41156,41157,41158,41159,41160,41161,41162,41163,41164,41165,41166,41167,41168,41169,41170,41171,41172,41173,41174,41175,41176,41177,41178,41179,41180,41181,41182,41183,41184,41185,41186,41187,41188,41189,41190,41191,41192,41193,41194,41195,41196,41197,41198,41199,41200,41201,41202,41203,41204,41205,41206,41207,41208,41209,41210,41211,41212,41213,41214,41215,41216,41217,41218,41219,41220,41221,41222,41223,41224,41225,41226,41227,41228,41229,41230,41231,41232,41233,41234,41235,41236,41237,41238,41239,41240,41241,41242,41243,41244,41245,41246,41247,41248,41249,41250,41251,41252,41253,41254,41255,41256,41257,41258,41259,41260,41261,41262,41263,41264,41265,41266,41267,41268,41269,41270,41271,41272,41273,41274,41275,41276,41277,41278,41279,41280,41281,41282,41283,41284,41285,41286,41287,41288,41289,41290,41291,41292,41293,41294,41295,41296,41297,41298,41299,41300,41301,41302,41303,41304,41305,41306,41307,41308,41309,41310,41311,41312,41313,41314,41315,41316,41317,41318,41319,41320,41321,41322,41323,41324,41325,41326,41327,41328,41329,41330,41331,41332,41333,41334,41335,41336,41337,41338,41339,41340,41341,41342,41343,41344,41345,41346,41347,41348,41349,41350,41351,41352,41353,41354,41355,41356,41357,41358,41359,41360,41361,41362,41363,41364,41365,41366,41367,41368,41369,41370,41371,41372,41373,41374,41375,41376,41377,41378,41379,41380,41381,41382,41383,41384,41385,41386,41387,41388,41389,41390,41391,41392,41393,41394,41395,41396,41397,41398,41399,41400,41401,41402,41403,41404,41405,41406,41407,41408,41409,41410,41411,41412,41413,41414,41415,41416,41417,41418,41419,41420,41421,41422,41423,41424,41425,41426,41427,41428,41429,41430,41431,41432,41433,41434,41435,41436,41437,41438,41439,41440,41441,41442,41443,41444,41445,41446,41447,41448,41449,41450,41451,41452,41453,41454,41455,41456,41457,41458,41459,41460,41461,41462,41463,41464,41465,41466,41467,41468,41469,41470,41471,41472,41473,41474,41475,41476,41477,41478,41479,41480,41481,41482,41483,41484,41485,41486,41487,41488,41489,41490,41491,41492,41493,41494,41495,41496,41497,41498,41499,41500,41501,41502,41503,41504,41505,41506,41507,41508,41509,41510,41511,41512,41513,41514,41515,41516,41517,41518,41519,41520,41521,41522,41523,41524,41525,41526,41527,41528,41529,41530,41531,41532,41533,41534,41535,41536,41537,41538,41539,41540,41541,41542,41543,41544,41545,41546,41547,41548,41549,41550,41551,41552,41553,41554,41555,41556,41557,41558,41559,41560,41561,41562,41563,41564,41565,41566,41567,41568,41569,41570,41571,41572,41573,41574,41575,41576,41577,41578,41579,41580,41581,41582,41583,41584,41585,41586,41587,41588,41589,41590,41591,41592,41593,41594,41595,41596,41597,41598,41599,41600,41601,41602,41603,41604,41605,41606,41607,41608,41609,41610,41611,41612,41613,41614,41615,41616,41617,41618,41619,41620,41621,41622,41623,41624,41625,41626,41627,41628,41629,41630,41631,41632,41633,41634,41635,41636,41637,41638,41639,41640,41641,41642,41643,41644,41645,41646,41647,41648,41649,41650,41651,41652,41653,41654,41655,41656,41657,41658,41659,41660,41661,41662,41663,41664,41665,41666,41667,41668,41669,41670,41671,41672,41673,41674,41675,41676,41677,41678,41679,41680,41681,41682,41683,41684,41685,41686,41687,41688,41689,41690,41691,41692,41693,41694,41695,41696,41697,41698,41699,41700,41701,41702,41703,41704,41705,41706,41707,41708,41709,41710,41711,41712,41713,41714,41715,41716,41717,41718,41719,41720,41721,41722,41723,41724,41725,41726,41727,41728,41729,41730,41731,41732,41733,41734,41735,41736,41737,41738,41739,41740,41741,41742,41743,41744,41745,41746,41747,41748,41749,41750,41751,41752,41753,41754,41755,41756,41757,41758,41759,41760,41761,41762,41763,41764,41765,41766,41767,41768,41769,41770,41771,41772,41773,41774,41775,41776,41777,41778,41779,41780,41781,41782,41783,41784,41785,41786,41787,41788,41789,41790,41791,41792,41793,41794,41795,41796,41797,41798,41799,41800,41801,41802,41803,41804,41805,41806,41807,41808,41809,41810,41811,41812,41813,41814,41815,41816,41817,41818,41819,41820,41821,41822,41823,41824,41825,41826,41827,41828,41829,41830,41831,41832,41833,41834,41835,41836,41837,41838,41839,41840,41841,41842,41843,41844,41845,41846,41847,41848,41849,41850,41851,41852,41853,41854,41855,41856,41857,41858,41859,41860,41861,41862,41863,41864,41865,41866,41867,41868,41869,41870,41871,41872,41873,41874,41875,41876,41877,41878,41879,41880,41881,41882,41883,41884,41885,41886,41887,41888,41889,41890,41891,41892,41893,41894,41895,41896,41897,41898,41899,41900,41901,41902,41903,41904,41905,41906,41907,41908,41909,41910,41911,41912,41913,41914,41915,41916,41917,41918,41919,41920,41921,41922,41923,41924,41925,41926,41927,41928,41929,41930,41931,41932,41933,41934,41935,41936,41937,41938,41939,41940,41941,41942,41943,41944,41945,41946,41947,41948,41949,41950,41951,41952,41953,41954,41955,41956,41957,41958,41959,41960,41961,41962,41963,41964,41965,41966,41967,41968,41969,41970,41971,41972,41973,41974,41975,41976,41977,41978,41979,41980,41981,41982,41983,41984,41985,41986,41987,41988,41989,41990,41991,41992,41993,41994,41995,41996,41997,41998,41999,42000,42001,42002,42003,42004,42005,42006,42007,42008,42009,42010,42011,42012,42013,42014,42015,42016,42017,42018,42019,42020,42021,42022,42023,42024,42025,42026,42027,42028,42029,42030,42031,42032,42033,42034,42035,42036,42037,42038,42039,42040,42041,42042,42043,42044,42045,42046,42047,42048,42049,42050,42051,42052,42053,42054,42055,42056,42057,42058,42059,42060,42061,42062,42063,42064,42065,42066,42067,42068,42069,42070,42071,42072,42073,42074,42075,42076,42077,42078,42079,42080,42081,42082,42083,42084,42085,42086,42087,42088,42089,42090,42091,42092,42093,42094,42095,42096,42097,42098,42099,42100,42101,42102,42103,42104,42105,42106,42107,42108,42109,42110,42111,42112,42113,42114,42115,42116,42117,42118,42119,42120,42121,42122,42123,42124,42192,42193,42194,42195,42196,42197,42198,42199,42200,42201,42202,42203,42204,42205,42206,42207,42208,42209,42210,42211,42212,42213,42214,42215,42216,42217,42218,42219,42220,42221,42222,42223,42224,42225,42226,42227,42228,42229,42230,42231,42232,42233,42234,42235,42236,42237,42240,42241,42242,42243,42244,42245,42246,42247,42248,42249,42250,42251,42252,42253,42254,42255,42256,42257,42258,42259,42260,42261,42262,42263,42264,42265,42266,42267,42268,42269,42270,42271,42272,42273,42274,42275,42276,42277,42278,42279,42280,42281,42282,42283,42284,42285,42286,42287,42288,42289,42290,42291,42292,42293,42294,42295,42296,42297,42298,42299,42300,42301,42302,42303,42304,42305,42306,42307,42308,42309,42310,42311,42312,42313,42314,42315,42316,42317,42318,42319,42320,42321,42322,42323,42324,42325,42326,42327,42328,42329,42330,42331,42332,42333,42334,42335,42336,42337,42338,42339,42340,42341,42342,42343,42344,42345,42346,42347,42348,42349,42350,42351,42352,42353,42354,42355,42356,42357,42358,42359,42360,42361,42362,42363,42364,42365,42366,42367,42368,42369,42370,42371,42372,42373,42374,42375,42376,42377,42378,42379,42380,42381,42382,42383,42384,42385,42386,42387,42388,42389,42390,42391,42392,42393,42394,42395,42396,42397,42398,42399,42400,42401,42402,42403,42404,42405,42406,42407,42408,42409,42410,42411,42412,42413,42414,42415,42416,42417,42418,42419,42420,42421,42422,42423,42424,42425,42426,42427,42428,42429,42430,42431,42432,42433,42434,42435,42436,42437,42438,42439,42440,42441,42442,42443,42444,42445,42446,42447,42448,42449,42450,42451,42452,42453,42454,42455,42456,42457,42458,42459,42460,42461,42462,42463,42464,42465,42466,42467,42468,42469,42470,42471,42472,42473,42474,42475,42476,42477,42478,42479,42480,42481,42482,42483,42484,42485,42486,42487,42488,42489,42490,42491,42492,42493,42494,42495,42496,42497,42498,42499,42500,42501,42502,42503,42504,42505,42506,42507,42508,42512,42513,42514,42515,42516,42517,42518,42519,42520,42521,42522,42523,42524,42525,42526,42527,42538,42539,42560,42561,42562,42563,42564,42565,42566,42567,42568,42569,42570,42571,42572,42573,42574,42575,42576,42577,42578,42579,42580,42581,42582,42583,42584,42585,42586,42587,42588,42589,42590,42591,42592,42593,42594,42595,42596,42597,42598,42599,42600,42601,42602,42603,42604,42605,42606,42623,42624,42625,42626,42627,42628,42629,42630,42631,42632,42633,42634,42635,42636,42637,42638,42639,42640,42641,42642,42643,42644,42645,42646,42647,42648,42649,42650,42651,42652,42653,42656,42657,42658,42659,42660,42661,42662,42663,42664,42665,42666,42667,42668,42669,42670,42671,42672,42673,42674,42675,42676,42677,42678,42679,42680,42681,42682,42683,42684,42685,42686,42687,42688,42689,42690,42691,42692,42693,42694,42695,42696,42697,42698,42699,42700,42701,42702,42703,42704,42705,42706,42707,42708,42709,42710,42711,42712,42713,42714,42715,42716,42717,42718,42719,42720,42721,42722,42723,42724,42725,42726,42727,42728,42729,42730,42731,42732,42733,42734,42735,42775,42776,42777,42778,42779,42780,42781,42782,42783,42786,42787,42788,42789,42790,42791,42792,42793,42794,42795,42796,42797,42798,42799,42800,42801,42802,42803,42804,42805,42806,42807,42808,42809,42810,42811,42812,42813,42814,42815,42816,42817,42818,42819,42820,42821,42822,42823,42824,42825,42826,42827,42828,42829,42830,42831,42832,42833,42834,42835,42836,42837,42838,42839,42840,42841,42842,42843,42844,42845,42846,42847,42848,42849,42850,42851,42852,42853,42854,42855,42856,42857,42858,42859,42860,42861,42862,42863,42864,42865,42866,42867,42868,42869,42870,42871,42872,42873,42874,42875,42876,42877,42878,42879,42880,42881,42882,42883,42884,42885,42886,42887,42888,42891,42892,42893,42894,42895,42896,42897,42898,42899,42900,42901,42902,42903,42904,42905,42906,42907,42908,42909,42910,42911,42912,42913,42914,42915,42916,42917,42918,42919,42920,42921,42922,42923,42924,42925,42926,42927,42928,42929,42930,42931,42932,42933,42934,42935,42936,42937,42999,43000,43001,43002,43003,43004,43005,43006,43007,43008,43009,43011,43012,43013,43015,43016,43017,43018,43020,43021,43022,43023,43024,43025,43026,43027,43028,43029,43030,43031,43032,43033,43034,43035,43036,43037,43038,43039,43040,43041,43042,43072,43073,43074,43075,43076,43077,43078,43079,43080,43081,43082,43083,43084,43085,43086,43087,43088,43089,43090,43091,43092,43093,43094,43095,43096,43097,43098,43099,43100,43101,43102,43103,43104,43105,43106,43107,43108,43109,43110,43111,43112,43113,43114,43115,43116,43117,43118,43119,43120,43121,43122,43123,43138,43139,43140,43141,43142,43143,43144,43145,43146,43147,43148,43149,43150,43151,43152,43153,43154,43155,43156,43157,43158,43159,43160,43161,43162,43163,43164,43165,43166,43167,43168,43169,43170,43171,43172,43173,43174,43175,43176,43177,43178,43179,43180,43181,43182,43183,43184,43185,43186,43187,43250,43251,43252,43253,43254,43255,43259,43261,43262,43274,43275,43276,43277,43278,43279,43280,43281,43282,43283,43284,43285,43286,43287,43288,43289,43290,43291,43292,43293,43294,43295,43296,43297,43298,43299,43300,43301,43312,43313,43314,43315,43316,43317,43318,43319,43320,43321,43322,43323,43324,43325,43326,43327,43328,43329,43330,43331,43332,43333,43334,43360,43361,43362,43363,43364,43365,43366,43367,43368,43369,43370,43371,43372,43373,43374,43375,43376,43377,43378,43379,43380,43381,43382,43383,43384,43385,43386,43387,43388,43396,43397,43398,43399,43400,43401,43402,43403,43404,43405,43406,43407,43408,43409,43410,43411,43412,43413,43414,43415,43416,43417,43418,43419,43420,43421,43422,43423,43424,43425,43426,43427,43428,43429,43430,43431,43432,43433,43434,43435,43436,43437,43438,43439,43440,43441,43442,43471,43488,43489,43490,43491,43492,43494,43495,43496,43497,43498,43499,43500,43501,43502,43503,43514,43515,43516,43517,43518,43520,43521,43522,43523,43524,43525,43526,43527,43528,43529,43530,43531,43532,43533,43534,43535,43536,43537,43538,43539,43540,43541,43542,43543,43544,43545,43546,43547,43548,43549,43550,43551,43552,43553,43554,43555,43556,43557,43558,43559,43560,43584,43585,43586,43588,43589,43590,43591,43592,43593,43594,43595,43616,43617,43618,43619,43620,43621,43622,43623,43624,43625,43626,43627,43628,43629,43630,43631,43632,43633,43634,43635,43636,43637,43638,43642,43646,43647,43648,43649,43650,43651,43652,43653,43654,43655,43656,43657,43658,43659,43660,43661,43662,43663,43664,43665,43666,43667,43668,43669,43670,43671,43672,43673,43674,43675,43676,43677,43678,43679,43680,43681,43682,43683,43684,43685,43686,43687,43688,43689,43690,43691,43692,43693,43694,43695,43697,43701,43702,43705,43706,43707,43708,43709,43712,43714,43739,43740,43741,43744,43745,43746,43747,43748,43749,43750,43751,43752,43753,43754,43762,43763,43764,43777,43778,43779,43780,43781,43782,43785,43786,43787,43788,43789,43790,43793,43794,43795,43796,43797,43798,43808,43809,43810,43811,43812,43813,43814,43816,43817,43818,43819,43820,43821,43822,43824,43825,43826,43827,43828,43829,43830,43831,43832,43833,43834,43835,43836,43837,43838,43839,43840,43841,43842,43843,43844,43845,43846,43847,43848,43849,43850,43851,43852,43853,43854,43855,43856,43857,43858,43859,43860,43861,43862,43863,43864,43865,43866,43868,43869,43870,43871,43872,43873,43874,43875,43876,43877,43888,43889,43890,43891,43892,43893,43894,43895,43896,43897,43898,43899,43900,43901,43902,43903,43904,43905,43906,43907,43908,43909,43910,43911,43912,43913,43914,43915,43916,43917,43918,43919,43920,43921,43922,43923,43924,43925,43926,43927,43928,43929,43930,43931,43932,43933,43934,43935,43936,43937,43938,43939,43940,43941,43942,43943,43944,43945,43946,43947,43948,43949,43950,43951,43952,43953,43954,43955,43956,43957,43958,43959,43960,43961,43962,43963,43964,43965,43966,43967,43968,43969,43970,43971,43972,43973,43974,43975,43976,43977,43978,43979,43980,43981,43982,43983,43984,43985,43986,43987,43988,43989,43990,43991,43992,43993,43994,43995,43996,43997,43998,43999,44000,44001,44002,44032,44033,44034,44035,44036,44037,44038,44039,44040,44041,44042,44043,44044,44045,44046,44047,44048,44049,44050,44051,44052,44053,44054,44055,44056,44057,44058,44059,44060,44061,44062,44063,44064,44065,44066,44067,44068,44069,44070,44071,44072,44073,44074,44075,44076,44077,44078,44079,44080,44081,44082,44083,44084,44085,44086,44087,44088,44089,44090,44091,44092,44093,44094,44095,44096,44097,44098,44099,44100,44101,44102,44103,44104,44105,44106,44107,44108,44109,44110,44111,44112,44113,44114,44115,44116,44117,44118,44119,44120,44121,44122,44123,44124,44125,44126,44127,44128,44129,44130,44131,44132,44133,44134,44135,44136,44137,44138,44139,44140,44141,44142,44143,44144,44145,44146,44147,44148,44149,44150,44151,44152,44153,44154,44155,44156,44157,44158,44159,44160,44161,44162,44163,44164,44165,44166,44167,44168,44169,44170,44171,44172,44173,44174,44175,44176,44177,44178,44179,44180,44181,44182,44183,44184,44185,44186,44187,44188,44189,44190,44191,44192,44193,44194,44195,44196,44197,44198,44199,44200,44201,44202,44203,44204,44205,44206,44207,44208,44209,44210,44211,44212,44213,44214,44215,44216,44217,44218,44219,44220,44221,44222,44223,44224,44225,44226,44227,44228,44229,44230,44231,44232,44233,44234,44235,44236,44237,44238,44239,44240,44241,44242,44243,44244,44245,44246,44247,44248,44249,44250,44251,44252,44253,44254,44255,44256,44257,44258,44259,44260,44261,44262,44263,44264,44265,44266,44267,44268,44269,44270,44271,44272,44273,44274,44275,44276,44277,44278,44279,44280,44281,44282,44283,44284,44285,44286,44287,44288,44289,44290,44291,44292,44293,44294,44295,44296,44297,44298,44299,44300,44301,44302,44303,44304,44305,44306,44307,44308,44309,44310,44311,44312,44313,44314,44315,44316,44317,44318,44319,44320,44321,44322,44323,44324,44325,44326,44327,44328,44329,44330,44331,44332,44333,44334,44335,44336,44337,44338,44339,44340,44341,44342,44343,44344,44345,44346,44347,44348,44349,44350,44351,44352,44353,44354,44355,44356,44357,44358,44359,44360,44361,44362,44363,44364,44365,44366,44367,44368,44369,44370,44371,44372,44373,44374,44375,44376,44377,44378,44379,44380,44381,44382,44383,44384,44385,44386,44387,44388,44389,44390,44391,44392,44393,44394,44395,44396,44397,44398,44399,44400,44401,44402,44403,44404,44405,44406,44407,44408,44409,44410,44411,44412,44413,44414,44415,44416,44417,44418,44419,44420,44421,44422,44423,44424,44425,44426,44427,44428,44429,44430,44431,44432,44433,44434,44435,44436,44437,44438,44439,44440,44441,44442,44443,44444,44445,44446,44447,44448,44449,44450,44451,44452,44453,44454,44455,44456,44457,44458,44459,44460,44461,44462,44463,44464,44465,44466,44467,44468,44469,44470,44471,44472,44473,44474,44475,44476,44477,44478,44479,44480,44481,44482,44483,44484,44485,44486,44487,44488,44489,44490,44491,44492,44493,44494,44495,44496,44497,44498,44499,44500,44501,44502,44503,44504,44505,44506,44507,44508,44509,44510,44511,44512,44513,44514,44515,44516,44517,44518,44519,44520,44521,44522,44523,44524,44525,44526,44527,44528,44529,44530,44531,44532,44533,44534,44535,44536,44537,44538,44539,44540,44541,44542,44543,44544,44545,44546,44547,44548,44549,44550,44551,44552,44553,44554,44555,44556,44557,44558,44559,44560,44561,44562,44563,44564,44565,44566,44567,44568,44569,44570,44571,44572,44573,44574,44575,44576,44577,44578,44579,44580,44581,44582,44583,44584,44585,44586,44587,44588,44589,44590,44591,44592,44593,44594,44595,44596,44597,44598,44599,44600,44601,44602,44603,44604,44605,44606,44607,44608,44609,44610,44611,44612,44613,44614,44615,44616,44617,44618,44619,44620,44621,44622,44623,44624,44625,44626,44627,44628,44629,44630,44631,44632,44633,44634,44635,44636,44637,44638,44639,44640,44641,44642,44643,44644,44645,44646,44647,44648,44649,44650,44651,44652,44653,44654,44655,44656,44657,44658,44659,44660,44661,44662,44663,44664,44665,44666,44667,44668,44669,44670,44671,44672,44673,44674,44675,44676,44677,44678,44679,44680,44681,44682,44683,44684,44685,44686,44687,44688,44689,44690,44691,44692,44693,44694,44695,44696,44697,44698,44699,44700,44701,44702,44703,44704,44705,44706,44707,44708,44709,44710,44711,44712,44713,44714,44715,44716,44717,44718,44719,44720,44721,44722,44723,44724,44725,44726,44727,44728,44729,44730,44731,44732,44733,44734,44735,44736,44737,44738,44739,44740,44741,44742,44743,44744,44745,44746,44747,44748,44749,44750,44751,44752,44753,44754,44755,44756,44757,44758,44759,44760,44761,44762,44763,44764,44765,44766,44767,44768,44769,44770,44771,44772,44773,44774,44775,44776,44777,44778,44779,44780,44781,44782,44783,44784,44785,44786,44787,44788,44789,44790,44791,44792,44793,44794,44795,44796,44797,44798,44799,44800,44801,44802,44803,44804,44805,44806,44807,44808,44809,44810,44811,44812,44813,44814,44815,44816,44817,44818,44819,44820,44821,44822,44823,44824,44825,44826,44827,44828,44829,44830,44831,44832,44833,44834,44835,44836,44837,44838,44839,44840,44841,44842,44843,44844,44845,44846,44847,44848,44849,44850,44851,44852,44853,44854,44855,44856,44857,44858,44859,44860,44861,44862,44863,44864,44865,44866,44867,44868,44869,44870,44871,44872,44873,44874,44875,44876,44877,44878,44879,44880,44881,44882,44883,44884,44885,44886,44887,44888,44889,44890,44891,44892,44893,44894,44895,44896,44897,44898,44899,44900,44901,44902,44903,44904,44905,44906,44907,44908,44909,44910,44911,44912,44913,44914,44915,44916,44917,44918,44919,44920,44921,44922,44923,44924,44925,44926,44927,44928,44929,44930,44931,44932,44933,44934,44935,44936,44937,44938,44939,44940,44941,44942,44943,44944,44945,44946,44947,44948,44949,44950,44951,44952,44953,44954,44955,44956,44957,44958,44959,44960,44961,44962,44963,44964,44965,44966,44967,44968,44969,44970,44971,44972,44973,44974,44975,44976,44977,44978,44979,44980,44981,44982,44983,44984,44985,44986,44987,44988,44989,44990,44991,44992,44993,44994,44995,44996,44997,44998,44999,45000,45001,45002,45003,45004,45005,45006,45007,45008,45009,45010,45011,45012,45013,45014,45015,45016,45017,45018,45019,45020,45021,45022,45023,45024,45025,45026,45027,45028,45029,45030,45031,45032,45033,45034,45035,45036,45037,45038,45039,45040,45041,45042,45043,45044,45045,45046,45047,45048,45049,45050,45051,45052,45053,45054,45055,45056,45057,45058,45059,45060,45061,45062,45063,45064,45065,45066,45067,45068,45069,45070,45071,45072,45073,45074,45075,45076,45077,45078,45079,45080,45081,45082,45083,45084,45085,45086,45087,45088,45089,45090,45091,45092,45093,45094,45095,45096,45097,45098,45099,45100,45101,45102,45103,45104,45105,45106,45107,45108,45109,45110,45111,45112,45113,45114,45115,45116,45117,45118,45119,45120,45121,45122,45123,45124,45125,45126,45127,45128,45129,45130,45131,45132,45133,45134,45135,45136,45137,45138,45139,45140,45141,45142,45143,45144,45145,45146,45147,45148,45149,45150,45151,45152,45153,45154,45155,45156,45157,45158,45159,45160,45161,45162,45163,45164,45165,45166,45167,45168,45169,45170,45171,45172,45173,45174,45175,45176,45177,45178,45179,45180,45181,45182,45183,45184,45185,45186,45187,45188,45189,45190,45191,45192,45193,45194,45195,45196,45197,45198,45199,45200,45201,45202,45203,45204,45205,45206,45207,45208,45209,45210,45211,45212,45213,45214,45215,45216,45217,45218,45219,45220,45221,45222,45223,45224,45225,45226,45227,45228,45229,45230,45231,45232,45233,45234,45235,45236,45237,45238,45239,45240,45241,45242,45243,45244,45245,45246,45247,45248,45249,45250,45251,45252,45253,45254,45255,45256,45257,45258,45259,45260,45261,45262,45263,45264,45265,45266,45267,45268,45269,45270,45271,45272,45273,45274,45275,45276,45277,45278,45279,45280,45281,45282,45283,45284,45285,45286,45287,45288,45289,45290,45291,45292,45293,45294,45295,45296,45297,45298,45299,45300,45301,45302,45303,45304,45305,45306,45307,45308,45309,45310,45311,45312,45313,45314,45315,45316,45317,45318,45319,45320,45321,45322,45323,45324,45325,45326,45327,45328,45329,45330,45331,45332,45333,45334,45335,45336,45337,45338,45339,45340,45341,45342,45343,45344,45345,45346,45347,45348,45349,45350,45351,45352,45353,45354,45355,45356,45357,45358,45359,45360,45361,45362,45363,45364,45365,45366,45367,45368,45369,45370,45371,45372,45373,45374,45375,45376,45377,45378,45379,45380,45381,45382,45383,45384,45385,45386,45387,45388,45389,45390,45391,45392,45393,45394,45395,45396,45397,45398,45399,45400,45401,45402,45403,45404,45405,45406,45407,45408,45409,45410,45411,45412,45413,45414,45415,45416,45417,45418,45419,45420,45421,45422,45423,45424,45425,45426,45427,45428,45429,45430,45431,45432,45433,45434,45435,45436,45437,45438,45439,45440,45441,45442,45443,45444,45445,45446,45447,45448,45449,45450,45451,45452,45453,45454,45455,45456,45457,45458,45459,45460,45461,45462,45463,45464,45465,45466,45467,45468,45469,45470,45471,45472,45473,45474,45475,45476,45477,45478,45479,45480,45481,45482,45483,45484,45485,45486,45487,45488,45489,45490,45491,45492,45493,45494,45495,45496,45497,45498,45499,45500,45501,45502,45503,45504,45505,45506,45507,45508,45509,45510,45511,45512,45513,45514,45515,45516,45517,45518,45519,45520,45521,45522,45523,45524,45525,45526,45527,45528,45529,45530,45531,45532,45533,45534,45535,45536,45537,45538,45539,45540,45541,45542,45543,45544,45545,45546,45547,45548,45549,45550,45551,45552,45553,45554,45555,45556,45557,45558,45559,45560,45561,45562,45563,45564,45565,45566,45567,45568,45569,45570,45571,45572,45573,45574,45575,45576,45577,45578,45579,45580,45581,45582,45583,45584,45585,45586,45587,45588,45589,45590,45591,45592,45593,45594,45595,45596,45597,45598,45599,45600,45601,45602,45603,45604,45605,45606,45607,45608,45609,45610,45611,45612,45613,45614,45615,45616,45617,45618,45619,45620,45621,45622,45623,45624,45625,45626,45627,45628,45629,45630,45631,45632,45633,45634,45635,45636,45637,45638,45639,45640,45641,45642,45643,45644,45645,45646,45647,45648,45649,45650,45651,45652,45653,45654,45655,45656,45657,45658,45659,45660,45661,45662,45663,45664,45665,45666,45667,45668,45669,45670,45671,45672,45673,45674,45675,45676,45677,45678,45679,45680,45681,45682,45683,45684,45685,45686,45687,45688,45689,45690,45691,45692,45693,45694,45695,45696,45697,45698,45699,45700,45701,45702,45703,45704,45705,45706,45707,45708,45709,45710,45711,45712,45713,45714,45715,45716,45717,45718,45719,45720,45721,45722,45723,45724,45725,45726,45727,45728,45729,45730,45731,45732,45733,45734,45735,45736,45737,45738,45739,45740,45741,45742,45743,45744,45745,45746,45747,45748,45749,45750,45751,45752,45753,45754,45755,45756,45757,45758,45759,45760,45761,45762,45763,45764,45765,45766,45767,45768,45769,45770,45771,45772,45773,45774,45775,45776,45777,45778,45779,45780,45781,45782,45783,45784,45785,45786,45787,45788,45789,45790,45791,45792,45793,45794,45795,45796,45797,45798,45799,45800,45801,45802,45803,45804,45805,45806,45807,45808,45809,45810,45811,45812,45813,45814,45815,45816,45817,45818,45819,45820,45821,45822,45823,45824,45825,45826,45827,45828,45829,45830,45831,45832,45833,45834,45835,45836,45837,45838,45839,45840,45841,45842,45843,45844,45845,45846,45847,45848,45849,45850,45851,45852,45853,45854,45855,45856,45857,45858,45859,45860,45861,45862,45863,45864,45865,45866,45867,45868,45869,45870,45871,45872,45873,45874,45875,45876,45877,45878,45879,45880,45881,45882,45883,45884,45885,45886,45887,45888,45889,45890,45891,45892,45893,45894,45895,45896,45897,45898,45899,45900,45901,45902,45903,45904,45905,45906,45907,45908,45909,45910,45911,45912,45913,45914,45915,45916,45917,45918,45919,45920,45921,45922,45923,45924,45925,45926,45927,45928,45929,45930,45931,45932,45933,45934,45935,45936,45937,45938,45939,45940,45941,45942,45943,45944,45945,45946,45947,45948,45949,45950,45951,45952,45953,45954,45955,45956,45957,45958,45959,45960,45961,45962,45963,45964,45965,45966,45967,45968,45969,45970,45971,45972,45973,45974,45975,45976,45977,45978,45979,45980,45981,45982,45983,45984,45985,45986,45987,45988,45989,45990,45991,45992,45993,45994,45995,45996,45997,45998,45999,46000,46001,46002,46003,46004,46005,46006,46007,46008,46009,46010,46011,46012,46013,46014,46015,46016,46017,46018,46019,46020,46021,46022,46023,46024,46025,46026,46027,46028,46029,46030,46031,46032,46033,46034,46035,46036,46037,46038,46039,46040,46041,46042,46043,46044,46045,46046,46047,46048,46049,46050,46051,46052,46053,46054,46055,46056,46057,46058,46059,46060,46061,46062,46063,46064,46065,46066,46067,46068,46069,46070,46071,46072,46073,46074,46075,46076,46077,46078,46079,46080,46081,46082,46083,46084,46085,46086,46087,46088,46089,46090,46091,46092,46093,46094,46095,46096,46097,46098,46099,46100,46101,46102,46103,46104,46105,46106,46107,46108,46109,46110,46111,46112,46113,46114,46115,46116,46117,46118,46119,46120,46121,46122,46123,46124,46125,46126,46127,46128,46129,46130,46131,46132,46133,46134,46135,46136,46137,46138,46139,46140,46141,46142,46143,46144,46145,46146,46147,46148,46149,46150,46151,46152,46153,46154,46155,46156,46157,46158,46159,46160,46161,46162,46163,46164,46165,46166,46167,46168,46169,46170,46171,46172,46173,46174,46175,46176,46177,46178,46179,46180,46181,46182,46183,46184,46185,46186,46187,46188,46189,46190,46191,46192,46193,46194,46195,46196,46197,46198,46199,46200,46201,46202,46203,46204,46205,46206,46207,46208,46209,46210,46211,46212,46213,46214,46215,46216,46217,46218,46219,46220,46221,46222,46223,46224,46225,46226,46227,46228,46229,46230,46231,46232,46233,46234,46235,46236,46237,46238,46239,46240,46241,46242,46243,46244,46245,46246,46247,46248,46249,46250,46251,46252,46253,46254,46255,46256,46257,46258,46259,46260,46261,46262,46263,46264,46265,46266,46267,46268,46269,46270,46271,46272,46273,46274,46275,46276,46277,46278,46279,46280,46281,46282,46283,46284,46285,46286,46287,46288,46289,46290,46291,46292,46293,46294,46295,46296,46297,46298,46299,46300,46301,46302,46303,46304,46305,46306,46307,46308,46309,46310,46311,46312,46313,46314,46315,46316,46317,46318,46319,46320,46321,46322,46323,46324,46325,46326,46327,46328,46329,46330,46331,46332,46333,46334,46335,46336,46337,46338,46339,46340,46341,46342,46343,46344,46345,46346,46347,46348,46349,46350,46351,46352,46353,46354,46355,46356,46357,46358,46359,46360,46361,46362,46363,46364,46365,46366,46367,46368,46369,46370,46371,46372,46373,46374,46375,46376,46377,46378,46379,46380,46381,46382,46383,46384,46385,46386,46387,46388,46389,46390,46391,46392,46393,46394,46395,46396,46397,46398,46399,46400,46401,46402,46403,46404,46405,46406,46407,46408,46409,46410,46411,46412,46413,46414,46415,46416,46417,46418,46419,46420,46421,46422,46423,46424,46425,46426,46427,46428,46429,46430,46431,46432,46433,46434,46435,46436,46437,46438,46439,46440,46441,46442,46443,46444,46445,46446,46447,46448,46449,46450,46451,46452,46453,46454,46455,46456,46457,46458,46459,46460,46461,46462,46463,46464,46465,46466,46467,46468,46469,46470,46471,46472,46473,46474,46475,46476,46477,46478,46479,46480,46481,46482,46483,46484,46485,46486,46487,46488,46489,46490,46491,46492,46493,46494,46495,46496,46497,46498,46499,46500,46501,46502,46503,46504,46505,46506,46507,46508,46509,46510,46511,46512,46513,46514,46515,46516,46517,46518,46519,46520,46521,46522,46523,46524,46525,46526,46527,46528,46529,46530,46531,46532,46533,46534,46535,46536,46537,46538,46539,46540,46541,46542,46543,46544,46545,46546,46547,46548,46549,46550,46551,46552,46553,46554,46555,46556,46557,46558,46559,46560,46561,46562,46563,46564,46565,46566,46567,46568,46569,46570,46571,46572,46573,46574,46575,46576,46577,46578,46579,46580,46581,46582,46583,46584,46585,46586,46587,46588,46589,46590,46591,46592,46593,46594,46595,46596,46597,46598,46599,46600,46601,46602,46603,46604,46605,46606,46607,46608,46609,46610,46611,46612,46613,46614,46615,46616,46617,46618,46619,46620,46621,46622,46623,46624,46625,46626,46627,46628,46629,46630,46631,46632,46633,46634,46635,46636,46637,46638,46639,46640,46641,46642,46643,46644,46645,46646,46647,46648,46649,46650,46651,46652,46653,46654,46655,46656,46657,46658,46659,46660,46661,46662,46663,46664,46665,46666,46667,46668,46669,46670,46671,46672,46673,46674,46675,46676,46677,46678,46679,46680,46681,46682,46683,46684,46685,46686,46687,46688,46689,46690,46691,46692,46693,46694,46695,46696,46697,46698,46699,46700,46701,46702,46703,46704,46705,46706,46707,46708,46709,46710,46711,46712,46713,46714,46715,46716,46717,46718,46719,46720,46721,46722,46723,46724,46725,46726,46727,46728,46729,46730,46731,46732,46733,46734,46735,46736,46737,46738,46739,46740,46741,46742,46743,46744,46745,46746,46747,46748,46749,46750,46751,46752,46753,46754,46755,46756,46757,46758,46759,46760,46761,46762,46763,46764,46765,46766,46767,46768,46769,46770,46771,46772,46773,46774,46775,46776,46777,46778,46779,46780,46781,46782,46783,46784,46785,46786,46787,46788,46789,46790,46791,46792,46793,46794,46795,46796,46797,46798,46799,46800,46801,46802,46803,46804,46805,46806,46807,46808,46809,46810,46811,46812,46813,46814,46815,46816,46817,46818,46819,46820,46821,46822,46823,46824,46825,46826,46827,46828,46829,46830,46831,46832,46833,46834,46835,46836,46837,46838,46839,46840,46841,46842,46843,46844,46845,46846,46847,46848,46849,46850,46851,46852,46853,46854,46855,46856,46857,46858,46859,46860,46861,46862,46863,46864,46865,46866,46867,46868,46869,46870,46871,46872,46873,46874,46875,46876,46877,46878,46879,46880,46881,46882,46883,46884,46885,46886,46887,46888,46889,46890,46891,46892,46893,46894,46895,46896,46897,46898,46899,46900,46901,46902,46903,46904,46905,46906,46907,46908,46909,46910,46911,46912,46913,46914,46915,46916,46917,46918,46919,46920,46921,46922,46923,46924,46925,46926,46927,46928,46929,46930,46931,46932,46933,46934,46935,46936,46937,46938,46939,46940,46941,46942,46943,46944,46945,46946,46947,46948,46949,46950,46951,46952,46953,46954,46955,46956,46957,46958,46959,46960,46961,46962,46963,46964,46965,46966,46967,46968,46969,46970,46971,46972,46973,46974,46975,46976,46977,46978,46979,46980,46981,46982,46983,46984,46985,46986,46987,46988,46989,46990,46991,46992,46993,46994,46995,46996,46997,46998,46999,47000,47001,47002,47003,47004,47005,47006,47007,47008,47009,47010,47011,47012,47013,47014,47015,47016,47017,47018,47019,47020,47021,47022,47023,47024,47025,47026,47027,47028,47029,47030,47031,47032,47033,47034,47035,47036,47037,47038,47039,47040,47041,47042,47043,47044,47045,47046,47047,47048,47049,47050,47051,47052,47053,47054,47055,47056,47057,47058,47059,47060,47061,47062,47063,47064,47065,47066,47067,47068,47069,47070,47071,47072,47073,47074,47075,47076,47077,47078,47079,47080,47081,47082,47083,47084,47085,47086,47087,47088,47089,47090,47091,47092,47093,47094,47095,47096,47097,47098,47099,47100,47101,47102,47103,47104,47105,47106,47107,47108,47109,47110,47111,47112,47113,47114,47115,47116,47117,47118,47119,47120,47121,47122,47123,47124,47125,47126,47127,47128,47129,47130,47131,47132,47133,47134,47135,47136,47137,47138,47139,47140,47141,47142,47143,47144,47145,47146,47147,47148,47149,47150,47151,47152,47153,47154,47155,47156,47157,47158,47159,47160,47161,47162,47163,47164,47165,47166,47167,47168,47169,47170,47171,47172,47173,47174,47175,47176,47177,47178,47179,47180,47181,47182,47183,47184,47185,47186,47187,47188,47189,47190,47191,47192,47193,47194,47195,47196,47197,47198,47199,47200,47201,47202,47203,47204,47205,47206,47207,47208,47209,47210,47211,47212,47213,47214,47215,47216,47217,47218,47219,47220,47221,47222,47223,47224,47225,47226,47227,47228,47229,47230,47231,47232,47233,47234,47235,47236,47237,47238,47239,47240,47241,47242,47243,47244,47245,47246,47247,47248,47249,47250,47251,47252,47253,47254,47255,47256,47257,47258,47259,47260,47261,47262,47263,47264,47265,47266,47267,47268,47269,47270,47271,47272,47273,47274,47275,47276,47277,47278,47279,47280,47281,47282,47283,47284,47285,47286,47287,47288,47289,47290,47291,47292,47293,47294,47295,47296,47297,47298,47299,47300,47301,47302,47303,47304,47305,47306,47307,47308,47309,47310,47311,47312,47313,47314,47315,47316,47317,47318,47319,47320,47321,47322,47323,47324,47325,47326,47327,47328,47329,47330,47331,47332,47333,47334,47335,47336,47337,47338,47339,47340,47341,47342,47343,47344,47345,47346,47347,47348,47349,47350,47351,47352,47353,47354,47355,47356,47357,47358,47359,47360,47361,47362,47363,47364,47365,47366,47367,47368,47369,47370,47371,47372,47373,47374,47375,47376,47377,47378,47379,47380,47381,47382,47383,47384,47385,47386,47387,47388,47389,47390,47391,47392,47393,47394,47395,47396,47397,47398,47399,47400,47401,47402,47403,47404,47405,47406,47407,47408,47409,47410,47411,47412,47413,47414,47415,47416,47417,47418,47419,47420,47421,47422,47423,47424,47425,47426,47427,47428,47429,47430,47431,47432,47433,47434,47435,47436,47437,47438,47439,47440,47441,47442,47443,47444,47445,47446,47447,47448,47449,47450,47451,47452,47453,47454,47455,47456,47457,47458,47459,47460,47461,47462,47463,47464,47465,47466,47467,47468,47469,47470,47471,47472,47473,47474,47475,47476,47477,47478,47479,47480,47481,47482,47483,47484,47485,47486,47487,47488,47489,47490,47491,47492,47493,47494,47495,47496,47497,47498,47499,47500,47501,47502,47503,47504,47505,47506,47507,47508,47509,47510,47511,47512,47513,47514,47515,47516,47517,47518,47519,47520,47521,47522,47523,47524,47525,47526,47527,47528,47529,47530,47531,47532,47533,47534,47535,47536,47537,47538,47539,47540,47541,47542,47543,47544,47545,47546,47547,47548,47549,47550,47551,47552,47553,47554,47555,47556,47557,47558,47559,47560,47561,47562,47563,47564,47565,47566,47567,47568,47569,47570,47571,47572,47573,47574,47575,47576,47577,47578,47579,47580,47581,47582,47583,47584,47585,47586,47587,47588,47589,47590,47591,47592,47593,47594,47595,47596,47597,47598,47599,47600,47601,47602,47603,47604,47605,47606,47607,47608,47609,47610,47611,47612,47613,47614,47615,47616,47617,47618,47619,47620,47621,47622,47623,47624,47625,47626,47627,47628,47629,47630,47631,47632,47633,47634,47635,47636,47637,47638,47639,47640,47641,47642,47643,47644,47645,47646,47647,47648,47649,47650,47651,47652,47653,47654,47655,47656,47657,47658,47659,47660,47661,47662,47663,47664,47665,47666,47667,47668,47669,47670,47671,47672,47673,47674,47675,47676,47677,47678,47679,47680,47681,47682,47683,47684,47685,47686,47687,47688,47689,47690,47691,47692,47693,47694,47695,47696,47697,47698,47699,47700,47701,47702,47703,47704,47705,47706,47707,47708,47709,47710,47711,47712,47713,47714,47715,47716,47717,47718,47719,47720,47721,47722,47723,47724,47725,47726,47727,47728,47729,47730,47731,47732,47733,47734,47735,47736,47737,47738,47739,47740,47741,47742,47743,47744,47745,47746,47747,47748,47749,47750,47751,47752,47753,47754,47755,47756,47757,47758,47759,47760,47761,47762,47763,47764,47765,47766,47767,47768,47769,47770,47771,47772,47773,47774,47775,47776,47777,47778,47779,47780,47781,47782,47783,47784,47785,47786,47787,47788,47789,47790,47791,47792,47793,47794,47795,47796,47797,47798,47799,47800,47801,47802,47803,47804,47805,47806,47807,47808,47809,47810,47811,47812,47813,47814,47815,47816,47817,47818,47819,47820,47821,47822,47823,47824,47825,47826,47827,47828,47829,47830,47831,47832,47833,47834,47835,47836,47837,47838,47839,47840,47841,47842,47843,47844,47845,47846,47847,47848,47849,47850,47851,47852,47853,47854,47855,47856,47857,47858,47859,47860,47861,47862,47863,47864,47865,47866,47867,47868,47869,47870,47871,47872,47873,47874,47875,47876,47877,47878,47879,47880,47881,47882,47883,47884,47885,47886,47887,47888,47889,47890,47891,47892,47893,47894,47895,47896,47897,47898,47899,47900,47901,47902,47903,47904,47905,47906,47907,47908,47909,47910,47911,47912,47913,47914,47915,47916,47917,47918,47919,47920,47921,47922,47923,47924,47925,47926,47927,47928,47929,47930,47931,47932,47933,47934,47935,47936,47937,47938,47939,47940,47941,47942,47943,47944,47945,47946,47947,47948,47949,47950,47951,47952,47953,47954,47955,47956,47957,47958,47959,47960,47961,47962,47963,47964,47965,47966,47967,47968,47969,47970,47971,47972,47973,47974,47975,47976,47977,47978,47979,47980,47981,47982,47983,47984,47985,47986,47987,47988,47989,47990,47991,47992,47993,47994,47995,47996,47997,47998,47999,48000,48001,48002,48003,48004,48005,48006,48007,48008,48009,48010,48011,48012,48013,48014,48015,48016,48017,48018,48019,48020,48021,48022,48023,48024,48025,48026,48027,48028,48029,48030,48031,48032,48033,48034,48035,48036,48037,48038,48039,48040,48041,48042,48043,48044,48045,48046,48047,48048,48049,48050,48051,48052,48053,48054,48055,48056,48057,48058,48059,48060,48061,48062,48063,48064,48065,48066,48067,48068,48069,48070,48071,48072,48073,48074,48075,48076,48077,48078,48079,48080,48081,48082,48083,48084,48085,48086,48087,48088,48089,48090,48091,48092,48093,48094,48095,48096,48097,48098,48099,48100,48101,48102,48103,48104,48105,48106,48107,48108,48109,48110,48111,48112,48113,48114,48115,48116,48117,48118,48119,48120,48121,48122,48123,48124,48125,48126,48127,48128,48129,48130,48131,48132,48133,48134,48135,48136,48137,48138,48139,48140,48141,48142,48143,48144,48145,48146,48147,48148,48149,48150,48151,48152,48153,48154,48155,48156,48157,48158,48159,48160,48161,48162,48163,48164,48165,48166,48167,48168,48169,48170,48171,48172,48173,48174,48175,48176,48177,48178,48179,48180,48181,48182,48183,48184,48185,48186,48187,48188,48189,48190,48191,48192,48193,48194,48195,48196,48197,48198,48199,48200,48201,48202,48203,48204,48205,48206,48207,48208,48209,48210,48211,48212,48213,48214,48215,48216,48217,48218,48219,48220,48221,48222,48223,48224,48225,48226,48227,48228,48229,48230,48231,48232,48233,48234,48235,48236,48237,48238,48239,48240,48241,48242,48243,48244,48245,48246,48247,48248,48249,48250,48251,48252,48253,48254,48255,48256,48257,48258,48259,48260,48261,48262,48263,48264,48265,48266,48267,48268,48269,48270,48271,48272,48273,48274,48275,48276,48277,48278,48279,48280,48281,48282,48283,48284,48285,48286,48287,48288,48289,48290,48291,48292,48293,48294,48295,48296,48297,48298,48299,48300,48301,48302,48303,48304,48305,48306,48307,48308,48309,48310,48311,48312,48313,48314,48315,48316,48317,48318,48319,48320,48321,48322,48323,48324,48325,48326,48327,48328,48329,48330,48331,48332,48333,48334,48335,48336,48337,48338,48339,48340,48341,48342,48343,48344,48345,48346,48347,48348,48349,48350,48351,48352,48353,48354,48355,48356,48357,48358,48359,48360,48361,48362,48363,48364,48365,48366,48367,48368,48369,48370,48371,48372,48373,48374,48375,48376,48377,48378,48379,48380,48381,48382,48383,48384,48385,48386,48387,48388,48389,48390,48391,48392,48393,48394,48395,48396,48397,48398,48399,48400,48401,48402,48403,48404,48405,48406,48407,48408,48409,48410,48411,48412,48413,48414,48415,48416,48417,48418,48419,48420,48421,48422,48423,48424,48425,48426,48427,48428,48429,48430,48431,48432,48433,48434,48435,48436,48437,48438,48439,48440,48441,48442,48443,48444,48445,48446,48447,48448,48449,48450,48451,48452,48453,48454,48455,48456,48457,48458,48459,48460,48461,48462,48463,48464,48465,48466,48467,48468,48469,48470,48471,48472,48473,48474,48475,48476,48477,48478,48479,48480,48481,48482,48483,48484,48485,48486,48487,48488,48489,48490,48491,48492,48493,48494,48495,48496,48497,48498,48499,48500,48501,48502,48503,48504,48505,48506,48507,48508,48509,48510,48511,48512,48513,48514,48515,48516,48517,48518,48519,48520,48521,48522,48523,48524,48525,48526,48527,48528,48529,48530,48531,48532,48533,48534,48535,48536,48537,48538,48539,48540,48541,48542,48543,48544,48545,48546,48547,48548,48549,48550,48551,48552,48553,48554,48555,48556,48557,48558,48559,48560,48561,48562,48563,48564,48565,48566,48567,48568,48569,48570,48571,48572,48573,48574,48575,48576,48577,48578,48579,48580,48581,48582,48583,48584,48585,48586,48587,48588,48589,48590,48591,48592,48593,48594,48595,48596,48597,48598,48599,48600,48601,48602,48603,48604,48605,48606,48607,48608,48609,48610,48611,48612,48613,48614,48615,48616,48617,48618,48619,48620,48621,48622,48623,48624,48625,48626,48627,48628,48629,48630,48631,48632,48633,48634,48635,48636,48637,48638,48639,48640,48641,48642,48643,48644,48645,48646,48647,48648,48649,48650,48651,48652,48653,48654,48655,48656,48657,48658,48659,48660,48661,48662,48663,48664,48665,48666,48667,48668,48669,48670,48671,48672,48673,48674,48675,48676,48677,48678,48679,48680,48681,48682,48683,48684,48685,48686,48687,48688,48689,48690,48691,48692,48693,48694,48695,48696,48697,48698,48699,48700,48701,48702,48703,48704,48705,48706,48707,48708,48709,48710,48711,48712,48713,48714,48715,48716,48717,48718,48719,48720,48721,48722,48723,48724,48725,48726,48727,48728,48729,48730,48731,48732,48733,48734,48735,48736,48737,48738,48739,48740,48741,48742,48743,48744,48745,48746,48747,48748,48749,48750,48751,48752,48753,48754,48755,48756,48757,48758,48759,48760,48761,48762,48763,48764,48765,48766,48767,48768,48769,48770,48771,48772,48773,48774,48775,48776,48777,48778,48779,48780,48781,48782,48783,48784,48785,48786,48787,48788,48789,48790,48791,48792,48793,48794,48795,48796,48797,48798,48799,48800,48801,48802,48803,48804,48805,48806,48807,48808,48809,48810,48811,48812,48813,48814,48815,48816,48817,48818,48819,48820,48821,48822,48823,48824,48825,48826,48827,48828,48829,48830,48831,48832,48833,48834,48835,48836,48837,48838,48839,48840,48841,48842,48843,48844,48845,48846,48847,48848,48849,48850,48851,48852,48853,48854,48855,48856,48857,48858,48859,48860,48861,48862,48863,48864,48865,48866,48867,48868,48869,48870,48871,48872,48873,48874,48875,48876,48877,48878,48879,48880,48881,48882,48883,48884,48885,48886,48887,48888,48889,48890,48891,48892,48893,48894,48895,48896,48897,48898,48899,48900,48901,48902,48903,48904,48905,48906,48907,48908,48909,48910,48911,48912,48913,48914,48915,48916,48917,48918,48919,48920,48921,48922,48923,48924,48925,48926,48927,48928,48929,48930,48931,48932,48933,48934,48935,48936,48937,48938,48939,48940,48941,48942,48943,48944,48945,48946,48947,48948,48949,48950,48951,48952,48953,48954,48955,48956,48957,48958,48959,48960,48961,48962,48963,48964,48965,48966,48967,48968,48969,48970,48971,48972,48973,48974,48975,48976,48977,48978,48979,48980,48981,48982,48983,48984,48985,48986,48987,48988,48989,48990,48991,48992,48993,48994,48995,48996,48997,48998,48999,49000,49001,49002,49003,49004,49005,49006,49007,49008,49009,49010,49011,49012,49013,49014,49015,49016,49017,49018,49019,49020,49021,49022,49023,49024,49025,49026,49027,49028,49029,49030,49031,49032,49033,49034,49035,49036,49037,49038,49039,49040,49041,49042,49043,49044,49045,49046,49047,49048,49049,49050,49051,49052,49053,49054,49055,49056,49057,49058,49059,49060,49061,49062,49063,49064,49065,49066,49067,49068,49069,49070,49071,49072,49073,49074,49075,49076,49077,49078,49079,49080,49081,49082,49083,49084,49085,49086,49087,49088,49089,49090,49091,49092,49093,49094,49095,49096,49097,49098,49099,49100,49101,49102,49103,49104,49105,49106,49107,49108,49109,49110,49111,49112,49113,49114,49115,49116,49117,49118,49119,49120,49121,49122,49123,49124,49125,49126,49127,49128,49129,49130,49131,49132,49133,49134,49135,49136,49137,49138,49139,49140,49141,49142,49143,49144,49145,49146,49147,49148,49149,49150,49151,49152,49153,49154,49155,49156,49157,49158,49159,49160,49161,49162,49163,49164,49165,49166,49167,49168,49169,49170,49171,49172,49173,49174,49175,49176,49177,49178,49179,49180,49181,49182,49183,49184,49185,49186,49187,49188,49189,49190,49191,49192,49193,49194,49195,49196,49197,49198,49199,49200,49201,49202,49203,49204,49205,49206,49207,49208,49209,49210,49211,49212,49213,49214,49215,49216,49217,49218,49219,49220,49221,49222,49223,49224,49225,49226,49227,49228,49229,49230,49231,49232,49233,49234,49235,49236,49237,49238,49239,49240,49241,49242,49243,49244,49245,49246,49247,49248,49249,49250,49251,49252,49253,49254,49255,49256,49257,49258,49259,49260,49261,49262,49263,49264,49265,49266,49267,49268,49269,49270,49271,49272,49273,49274,49275,49276,49277,49278,49279,49280,49281,49282,49283,49284,49285,49286,49287,49288,49289,49290,49291,49292,49293,49294,49295,49296,49297,49298,49299,49300,49301,49302,49303,49304,49305,49306,49307,49308,49309,49310,49311,49312,49313,49314,49315,49316,49317,49318,49319,49320,49321,49322,49323,49324,49325,49326,49327,49328,49329,49330,49331,49332,49333,49334,49335,49336,49337,49338,49339,49340,49341,49342,49343,49344,49345,49346,49347,49348,49349,49350,49351,49352,49353,49354,49355,49356,49357,49358,49359,49360,49361,49362,49363,49364,49365,49366,49367,49368,49369,49370,49371,49372,49373,49374,49375,49376,49377,49378,49379,49380,49381,49382,49383,49384,49385,49386,49387,49388,49389,49390,49391,49392,49393,49394,49395,49396,49397,49398,49399,49400,49401,49402,49403,49404,49405,49406,49407,49408,49409,49410,49411,49412,49413,49414,49415,49416,49417,49418,49419,49420,49421,49422,49423,49424,49425,49426,49427,49428,49429,49430,49431,49432,49433,49434,49435,49436,49437,49438,49439,49440,49441,49442,49443,49444,49445,49446,49447,49448,49449,49450,49451,49452,49453,49454,49455,49456,49457,49458,49459,49460,49461,49462,49463,49464,49465,49466,49467,49468,49469,49470,49471,49472,49473,49474,49475,49476,49477,49478,49479,49480,49481,49482,49483,49484,49485,49486,49487,49488,49489,49490,49491,49492,49493,49494,49495,49496,49497,49498,49499,49500,49501,49502,49503,49504,49505,49506,49507,49508,49509,49510,49511,49512,49513,49514,49515,49516,49517,49518,49519,49520,49521,49522,49523,49524,49525,49526,49527,49528,49529,49530,49531,49532,49533,49534,49535,49536,49537,49538,49539,49540,49541,49542,49543,49544,49545,49546,49547,49548,49549,49550,49551,49552,49553,49554,49555,49556,49557,49558,49559,49560,49561,49562,49563,49564,49565,49566,49567,49568,49569,49570,49571,49572,49573,49574,49575,49576,49577,49578,49579,49580,49581,49582,49583,49584,49585,49586,49587,49588,49589,49590,49591,49592,49593,49594,49595,49596,49597,49598,49599,49600,49601,49602,49603,49604,49605,49606,49607,49608,49609,49610,49611,49612,49613,49614,49615,49616,49617,49618,49619,49620,49621,49622,49623,49624,49625,49626,49627,49628,49629,49630,49631,49632,49633,49634,49635,49636,49637,49638,49639,49640,49641,49642,49643,49644,49645,49646,49647,49648,49649,49650,49651,49652,49653,49654,49655,49656,49657,49658,49659,49660,49661,49662,49663,49664,49665,49666,49667,49668,49669,49670,49671,49672,49673,49674,49675,49676,49677,49678,49679,49680,49681,49682,49683,49684,49685,49686,49687,49688,49689,49690,49691,49692,49693,49694,49695,49696,49697,49698,49699,49700,49701,49702,49703,49704,49705,49706,49707,49708,49709,49710,49711,49712,49713,49714,49715,49716,49717,49718,49719,49720,49721,49722,49723,49724,49725,49726,49727,49728,49729,49730,49731,49732,49733,49734,49735,49736,49737,49738,49739,49740,49741,49742,49743,49744,49745,49746,49747,49748,49749,49750,49751,49752,49753,49754,49755,49756,49757,49758,49759,49760,49761,49762,49763,49764,49765,49766,49767,49768,49769,49770,49771,49772,49773,49774,49775,49776,49777,49778,49779,49780,49781,49782,49783,49784,49785,49786,49787,49788,49789,49790,49791,49792,49793,49794,49795,49796,49797,49798,49799,49800,49801,49802,49803,49804,49805,49806,49807,49808,49809,49810,49811,49812,49813,49814,49815,49816,49817,49818,49819,49820,49821,49822,49823,49824,49825,49826,49827,49828,49829,49830,49831,49832,49833,49834,49835,49836,49837,49838,49839,49840,49841,49842,49843,49844,49845,49846,49847,49848,49849,49850,49851,49852,49853,49854,49855,49856,49857,49858,49859,49860,49861,49862,49863,49864,49865,49866,49867,49868,49869,49870,49871,49872,49873,49874,49875,49876,49877,49878,49879,49880,49881,49882,49883,49884,49885,49886,49887,49888,49889,49890,49891,49892,49893,49894,49895,49896,49897,49898,49899,49900,49901,49902,49903,49904,49905,49906,49907,49908,49909,49910,49911,49912,49913,49914,49915,49916,49917,49918,49919,49920,49921,49922,49923,49924,49925,49926,49927,49928,49929,49930,49931,49932,49933,49934,49935,49936,49937,49938,49939,49940,49941,49942,49943,49944,49945,49946,49947,49948,49949,49950,49951,49952,49953,49954,49955,49956,49957,49958,49959,49960,49961,49962,49963,49964,49965,49966,49967,49968,49969,49970,49971,49972,49973,49974,49975,49976,49977,49978,49979,49980,49981,49982,49983,49984,49985,49986,49987,49988,49989,49990,49991,49992,49993,49994,49995,49996,49997,49998,49999,50000,50001,50002,50003,50004,50005,50006,50007,50008,50009,50010,50011,50012,50013,50014,50015,50016,50017,50018,50019,50020,50021,50022,50023,50024,50025,50026,50027,50028,50029,50030,50031,50032,50033,50034,50035,50036,50037,50038,50039,50040,50041,50042,50043,50044,50045,50046,50047,50048,50049,50050,50051,50052,50053,50054,50055,50056,50057,50058,50059,50060,50061,50062,50063,50064,50065,50066,50067,50068,50069,50070,50071,50072,50073,50074,50075,50076,50077,50078,50079,50080,50081,50082,50083,50084,50085,50086,50087,50088,50089,50090,50091,50092,50093,50094,50095,50096,50097,50098,50099,50100,50101,50102,50103,50104,50105,50106,50107,50108,50109,50110,50111,50112,50113,50114,50115,50116,50117,50118,50119,50120,50121,50122,50123,50124,50125,50126,50127,50128,50129,50130,50131,50132,50133,50134,50135,50136,50137,50138,50139,50140,50141,50142,50143,50144,50145,50146,50147,50148,50149,50150,50151,50152,50153,50154,50155,50156,50157,50158,50159,50160,50161,50162,50163,50164,50165,50166,50167,50168,50169,50170,50171,50172,50173,50174,50175,50176,50177,50178,50179,50180,50181,50182,50183,50184,50185,50186,50187,50188,50189,50190,50191,50192,50193,50194,50195,50196,50197,50198,50199,50200,50201,50202,50203,50204,50205,50206,50207,50208,50209,50210,50211,50212,50213,50214,50215,50216,50217,50218,50219,50220,50221,50222,50223,50224,50225,50226,50227,50228,50229,50230,50231,50232,50233,50234,50235,50236,50237,50238,50239,50240,50241,50242,50243,50244,50245,50246,50247,50248,50249,50250,50251,50252,50253,50254,50255,50256,50257,50258,50259,50260,50261,50262,50263,50264,50265,50266,50267,50268,50269,50270,50271,50272,50273,50274,50275,50276,50277,50278,50279,50280,50281,50282,50283,50284,50285,50286,50287,50288,50289,50290,50291,50292,50293,50294,50295,50296,50297,50298,50299,50300,50301,50302,50303,50304,50305,50306,50307,50308,50309,50310,50311,50312,50313,50314,50315,50316,50317,50318,50319,50320,50321,50322,50323,50324,50325,50326,50327,50328,50329,50330,50331,50332,50333,50334,50335,50336,50337,50338,50339,50340,50341,50342,50343,50344,50345,50346,50347,50348,50349,50350,50351,50352,50353,50354,50355,50356,50357,50358,50359,50360,50361,50362,50363,50364,50365,50366,50367,50368,50369,50370,50371,50372,50373,50374,50375,50376,50377,50378,50379,50380,50381,50382,50383,50384,50385,50386,50387,50388,50389,50390,50391,50392,50393,50394,50395,50396,50397,50398,50399,50400,50401,50402,50403,50404,50405,50406,50407,50408,50409,50410,50411,50412,50413,50414,50415,50416,50417,50418,50419,50420,50421,50422,50423,50424,50425,50426,50427,50428,50429,50430,50431,50432,50433,50434,50435,50436,50437,50438,50439,50440,50441,50442,50443,50444,50445,50446,50447,50448,50449,50450,50451,50452,50453,50454,50455,50456,50457,50458,50459,50460,50461,50462,50463,50464,50465,50466,50467,50468,50469,50470,50471,50472,50473,50474,50475,50476,50477,50478,50479,50480,50481,50482,50483,50484,50485,50486,50487,50488,50489,50490,50491,50492,50493,50494,50495,50496,50497,50498,50499,50500,50501,50502,50503,50504,50505,50506,50507,50508,50509,50510,50511,50512,50513,50514,50515,50516,50517,50518,50519,50520,50521,50522,50523,50524,50525,50526,50527,50528,50529,50530,50531,50532,50533,50534,50535,50536,50537,50538,50539,50540,50541,50542,50543,50544,50545,50546,50547,50548,50549,50550,50551,50552,50553,50554,50555,50556,50557,50558,50559,50560,50561,50562,50563,50564,50565,50566,50567,50568,50569,50570,50571,50572,50573,50574,50575,50576,50577,50578,50579,50580,50581,50582,50583,50584,50585,50586,50587,50588,50589,50590,50591,50592,50593,50594,50595,50596,50597,50598,50599,50600,50601,50602,50603,50604,50605,50606,50607,50608,50609,50610,50611,50612,50613,50614,50615,50616,50617,50618,50619,50620,50621,50622,50623,50624,50625,50626,50627,50628,50629,50630,50631,50632,50633,50634,50635,50636,50637,50638,50639,50640,50641,50642,50643,50644,50645,50646,50647,50648,50649,50650,50651,50652,50653,50654,50655,50656,50657,50658,50659,50660,50661,50662,50663,50664,50665,50666,50667,50668,50669,50670,50671,50672,50673,50674,50675,50676,50677,50678,50679,50680,50681,50682,50683,50684,50685,50686,50687,50688,50689,50690,50691,50692,50693,50694,50695,50696,50697,50698,50699,50700,50701,50702,50703,50704,50705,50706,50707,50708,50709,50710,50711,50712,50713,50714,50715,50716,50717,50718,50719,50720,50721,50722,50723,50724,50725,50726,50727,50728,50729,50730,50731,50732,50733,50734,50735,50736,50737,50738,50739,50740,50741,50742,50743,50744,50745,50746,50747,50748,50749,50750,50751,50752,50753,50754,50755,50756,50757,50758,50759,50760,50761,50762,50763,50764,50765,50766,50767,50768,50769,50770,50771,50772,50773,50774,50775,50776,50777,50778,50779,50780,50781,50782,50783,50784,50785,50786,50787,50788,50789,50790,50791,50792,50793,50794,50795,50796,50797,50798,50799,50800,50801,50802,50803,50804,50805,50806,50807,50808,50809,50810,50811,50812,50813,50814,50815,50816,50817,50818,50819,50820,50821,50822,50823,50824,50825,50826,50827,50828,50829,50830,50831,50832,50833,50834,50835,50836,50837,50838,50839,50840,50841,50842,50843,50844,50845,50846,50847,50848,50849,50850,50851,50852,50853,50854,50855,50856,50857,50858,50859,50860,50861,50862,50863,50864,50865,50866,50867,50868,50869,50870,50871,50872,50873,50874,50875,50876,50877,50878,50879,50880,50881,50882,50883,50884,50885,50886,50887,50888,50889,50890,50891,50892,50893,50894,50895,50896,50897,50898,50899,50900,50901,50902,50903,50904,50905,50906,50907,50908,50909,50910,50911,50912,50913,50914,50915,50916,50917,50918,50919,50920,50921,50922,50923,50924,50925,50926,50927,50928,50929,50930,50931,50932,50933,50934,50935,50936,50937,50938,50939,50940,50941,50942,50943,50944,50945,50946,50947,50948,50949,50950,50951,50952,50953,50954,50955,50956,50957,50958,50959,50960,50961,50962,50963,50964,50965,50966,50967,50968,50969,50970,50971,50972,50973,50974,50975,50976,50977,50978,50979,50980,50981,50982,50983,50984,50985,50986,50987,50988,50989,50990,50991,50992,50993,50994,50995,50996,50997,50998,50999,51000,51001,51002,51003,51004,51005,51006,51007,51008,51009,51010,51011,51012,51013,51014,51015,51016,51017,51018,51019,51020,51021,51022,51023,51024,51025,51026,51027,51028,51029,51030,51031,51032,51033,51034,51035,51036,51037,51038,51039,51040,51041,51042,51043,51044,51045,51046,51047,51048,51049,51050,51051,51052,51053,51054,51055,51056,51057,51058,51059,51060,51061,51062,51063,51064,51065,51066,51067,51068,51069,51070,51071,51072,51073,51074,51075,51076,51077,51078,51079,51080,51081,51082,51083,51084,51085,51086,51087,51088,51089,51090,51091,51092,51093,51094,51095,51096,51097,51098,51099,51100,51101,51102,51103,51104,51105,51106,51107,51108,51109,51110,51111,51112,51113,51114,51115,51116,51117,51118,51119,51120,51121,51122,51123,51124,51125,51126,51127,51128,51129,51130,51131,51132,51133,51134,51135,51136,51137,51138,51139,51140,51141,51142,51143,51144,51145,51146,51147,51148,51149,51150,51151,51152,51153,51154,51155,51156,51157,51158,51159,51160,51161,51162,51163,51164,51165,51166,51167,51168,51169,51170,51171,51172,51173,51174,51175,51176,51177,51178,51179,51180,51181,51182,51183,51184,51185,51186,51187,51188,51189,51190,51191,51192,51193,51194,51195,51196,51197,51198,51199,51200,51201,51202,51203,51204,51205,51206,51207,51208,51209,51210,51211,51212,51213,51214,51215,51216,51217,51218,51219,51220,51221,51222,51223,51224,51225,51226,51227,51228,51229,51230,51231,51232,51233,51234,51235,51236,51237,51238,51239,51240,51241,51242,51243,51244,51245,51246,51247,51248,51249,51250,51251,51252,51253,51254,51255,51256,51257,51258,51259,51260,51261,51262,51263,51264,51265,51266,51267,51268,51269,51270,51271,51272,51273,51274,51275,51276,51277,51278,51279,51280,51281,51282,51283,51284,51285,51286,51287,51288,51289,51290,51291,51292,51293,51294,51295,51296,51297,51298,51299,51300,51301,51302,51303,51304,51305,51306,51307,51308,51309,51310,51311,51312,51313,51314,51315,51316,51317,51318,51319,51320,51321,51322,51323,51324,51325,51326,51327,51328,51329,51330,51331,51332,51333,51334,51335,51336,51337,51338,51339,51340,51341,51342,51343,51344,51345,51346,51347,51348,51349,51350,51351,51352,51353,51354,51355,51356,51357,51358,51359,51360,51361,51362,51363,51364,51365,51366,51367,51368,51369,51370,51371,51372,51373,51374,51375,51376,51377,51378,51379,51380,51381,51382,51383,51384,51385,51386,51387,51388,51389,51390,51391,51392,51393,51394,51395,51396,51397,51398,51399,51400,51401,51402,51403,51404,51405,51406,51407,51408,51409,51410,51411,51412,51413,51414,51415,51416,51417,51418,51419,51420,51421,51422,51423,51424,51425,51426,51427,51428,51429,51430,51431,51432,51433,51434,51435,51436,51437,51438,51439,51440,51441,51442,51443,51444,51445,51446,51447,51448,51449,51450,51451,51452,51453,51454,51455,51456,51457,51458,51459,51460,51461,51462,51463,51464,51465,51466,51467,51468,51469,51470,51471,51472,51473,51474,51475,51476,51477,51478,51479,51480,51481,51482,51483,51484,51485,51486,51487,51488,51489,51490,51491,51492,51493,51494,51495,51496,51497,51498,51499,51500,51501,51502,51503,51504,51505,51506,51507,51508,51509,51510,51511,51512,51513,51514,51515,51516,51517,51518,51519,51520,51521,51522,51523,51524,51525,51526,51527,51528,51529,51530,51531,51532,51533,51534,51535,51536,51537,51538,51539,51540,51541,51542,51543,51544,51545,51546,51547,51548,51549,51550,51551,51552,51553,51554,51555,51556,51557,51558,51559,51560,51561,51562,51563,51564,51565,51566,51567,51568,51569,51570,51571,51572,51573,51574,51575,51576,51577,51578,51579,51580,51581,51582,51583,51584,51585,51586,51587,51588,51589,51590,51591,51592,51593,51594,51595,51596,51597,51598,51599,51600,51601,51602,51603,51604,51605,51606,51607,51608,51609,51610,51611,51612,51613,51614,51615,51616,51617,51618,51619,51620,51621,51622,51623,51624,51625,51626,51627,51628,51629,51630,51631,51632,51633,51634,51635,51636,51637,51638,51639,51640,51641,51642,51643,51644,51645,51646,51647,51648,51649,51650,51651,51652,51653,51654,51655,51656,51657,51658,51659,51660,51661,51662,51663,51664,51665,51666,51667,51668,51669,51670,51671,51672,51673,51674,51675,51676,51677,51678,51679,51680,51681,51682,51683,51684,51685,51686,51687,51688,51689,51690,51691,51692,51693,51694,51695,51696,51697,51698,51699,51700,51701,51702,51703,51704,51705,51706,51707,51708,51709,51710,51711,51712,51713,51714,51715,51716,51717,51718,51719,51720,51721,51722,51723,51724,51725,51726,51727,51728,51729,51730,51731,51732,51733,51734,51735,51736,51737,51738,51739,51740,51741,51742,51743,51744,51745,51746,51747,51748,51749,51750,51751,51752,51753,51754,51755,51756,51757,51758,51759,51760,51761,51762,51763,51764,51765,51766,51767,51768,51769,51770,51771,51772,51773,51774,51775,51776,51777,51778,51779,51780,51781,51782,51783,51784,51785,51786,51787,51788,51789,51790,51791,51792,51793,51794,51795,51796,51797,51798,51799,51800,51801,51802,51803,51804,51805,51806,51807,51808,51809,51810,51811,51812,51813,51814,51815,51816,51817,51818,51819,51820,51821,51822,51823,51824,51825,51826,51827,51828,51829,51830,51831,51832,51833,51834,51835,51836,51837,51838,51839,51840,51841,51842,51843,51844,51845,51846,51847,51848,51849,51850,51851,51852,51853,51854,51855,51856,51857,51858,51859,51860,51861,51862,51863,51864,51865,51866,51867,51868,51869,51870,51871,51872,51873,51874,51875,51876,51877,51878,51879,51880,51881,51882,51883,51884,51885,51886,51887,51888,51889,51890,51891,51892,51893,51894,51895,51896,51897,51898,51899,51900,51901,51902,51903,51904,51905,51906,51907,51908,51909,51910,51911,51912,51913,51914,51915,51916,51917,51918,51919,51920,51921,51922,51923,51924,51925,51926,51927,51928,51929,51930,51931,51932,51933,51934,51935,51936,51937,51938,51939,51940,51941,51942,51943,51944,51945,51946,51947,51948,51949,51950,51951,51952,51953,51954,51955,51956,51957,51958,51959,51960,51961,51962,51963,51964,51965,51966,51967,51968,51969,51970,51971,51972,51973,51974,51975,51976,51977,51978,51979,51980,51981,51982,51983,51984,51985,51986,51987,51988,51989,51990,51991,51992,51993,51994,51995,51996,51997,51998,51999,52000,52001,52002,52003,52004,52005,52006,52007,52008,52009,52010,52011,52012,52013,52014,52015,52016,52017,52018,52019,52020,52021,52022,52023,52024,52025,52026,52027,52028,52029,52030,52031,52032,52033,52034,52035,52036,52037,52038,52039,52040,52041,52042,52043,52044,52045,52046,52047,52048,52049,52050,52051,52052,52053,52054,52055,52056,52057,52058,52059,52060,52061,52062,52063,52064,52065,52066,52067,52068,52069,52070,52071,52072,52073,52074,52075,52076,52077,52078,52079,52080,52081,52082,52083,52084,52085,52086,52087,52088,52089,52090,52091,52092,52093,52094,52095,52096,52097,52098,52099,52100,52101,52102,52103,52104,52105,52106,52107,52108,52109,52110,52111,52112,52113,52114,52115,52116,52117,52118,52119,52120,52121,52122,52123,52124,52125,52126,52127,52128,52129,52130,52131,52132,52133,52134,52135,52136,52137,52138,52139,52140,52141,52142,52143,52144,52145,52146,52147,52148,52149,52150,52151,52152,52153,52154,52155,52156,52157,52158,52159,52160,52161,52162,52163,52164,52165,52166,52167,52168,52169,52170,52171,52172,52173,52174,52175,52176,52177,52178,52179,52180,52181,52182,52183,52184,52185,52186,52187,52188,52189,52190,52191,52192,52193,52194,52195,52196,52197,52198,52199,52200,52201,52202,52203,52204,52205,52206,52207,52208,52209,52210,52211,52212,52213,52214,52215,52216,52217,52218,52219,52220,52221,52222,52223,52224,52225,52226,52227,52228,52229,52230,52231,52232,52233,52234,52235,52236,52237,52238,52239,52240,52241,52242,52243,52244,52245,52246,52247,52248,52249,52250,52251,52252,52253,52254,52255,52256,52257,52258,52259,52260,52261,52262,52263,52264,52265,52266,52267,52268,52269,52270,52271,52272,52273,52274,52275,52276,52277,52278,52279,52280,52281,52282,52283,52284,52285,52286,52287,52288,52289,52290,52291,52292,52293,52294,52295,52296,52297,52298,52299,52300,52301,52302,52303,52304,52305,52306,52307,52308,52309,52310,52311,52312,52313,52314,52315,52316,52317,52318,52319,52320,52321,52322,52323,52324,52325,52326,52327,52328,52329,52330,52331,52332,52333,52334,52335,52336,52337,52338,52339,52340,52341,52342,52343,52344,52345,52346,52347,52348,52349,52350,52351,52352,52353,52354,52355,52356,52357,52358,52359,52360,52361,52362,52363,52364,52365,52366,52367,52368,52369,52370,52371,52372,52373,52374,52375,52376,52377,52378,52379,52380,52381,52382,52383,52384,52385,52386,52387,52388,52389,52390,52391,52392,52393,52394,52395,52396,52397,52398,52399,52400,52401,52402,52403,52404,52405,52406,52407,52408,52409,52410,52411,52412,52413,52414,52415,52416,52417,52418,52419,52420,52421,52422,52423,52424,52425,52426,52427,52428,52429,52430,52431,52432,52433,52434,52435,52436,52437,52438,52439,52440,52441,52442,52443,52444,52445,52446,52447,52448,52449,52450,52451,52452,52453,52454,52455,52456,52457,52458,52459,52460,52461,52462,52463,52464,52465,52466,52467,52468,52469,52470,52471,52472,52473,52474,52475,52476,52477,52478,52479,52480,52481,52482,52483,52484,52485,52486,52487,52488,52489,52490,52491,52492,52493,52494,52495,52496,52497,52498,52499,52500,52501,52502,52503,52504,52505,52506,52507,52508,52509,52510,52511,52512,52513,52514,52515,52516,52517,52518,52519,52520,52521,52522,52523,52524,52525,52526,52527,52528,52529,52530,52531,52532,52533,52534,52535,52536,52537,52538,52539,52540,52541,52542,52543,52544,52545,52546,52547,52548,52549,52550,52551,52552,52553,52554,52555,52556,52557,52558,52559,52560,52561,52562,52563,52564,52565,52566,52567,52568,52569,52570,52571,52572,52573,52574,52575,52576,52577,52578,52579,52580,52581,52582,52583,52584,52585,52586,52587,52588,52589,52590,52591,52592,52593,52594,52595,52596,52597,52598,52599,52600,52601,52602,52603,52604,52605,52606,52607,52608,52609,52610,52611,52612,52613,52614,52615,52616,52617,52618,52619,52620,52621,52622,52623,52624,52625,52626,52627,52628,52629,52630,52631,52632,52633,52634,52635,52636,52637,52638,52639,52640,52641,52642,52643,52644,52645,52646,52647,52648,52649,52650,52651,52652,52653,52654,52655,52656,52657,52658,52659,52660,52661,52662,52663,52664,52665,52666,52667,52668,52669,52670,52671,52672,52673,52674,52675,52676,52677,52678,52679,52680,52681,52682,52683,52684,52685,52686,52687,52688,52689,52690,52691,52692,52693,52694,52695,52696,52697,52698,52699,52700,52701,52702,52703,52704,52705,52706,52707,52708,52709,52710,52711,52712,52713,52714,52715,52716,52717,52718,52719,52720,52721,52722,52723,52724,52725,52726,52727,52728,52729,52730,52731,52732,52733,52734,52735,52736,52737,52738,52739,52740,52741,52742,52743,52744,52745,52746,52747,52748,52749,52750,52751,52752,52753,52754,52755,52756,52757,52758,52759,52760,52761,52762,52763,52764,52765,52766,52767,52768,52769,52770,52771,52772,52773,52774,52775,52776,52777,52778,52779,52780,52781,52782,52783,52784,52785,52786,52787,52788,52789,52790,52791,52792,52793,52794,52795,52796,52797,52798,52799,52800,52801,52802,52803,52804,52805,52806,52807,52808,52809,52810,52811,52812,52813,52814,52815,52816,52817,52818,52819,52820,52821,52822,52823,52824,52825,52826,52827,52828,52829,52830,52831,52832,52833,52834,52835,52836,52837,52838,52839,52840,52841,52842,52843,52844,52845,52846,52847,52848,52849,52850,52851,52852,52853,52854,52855,52856,52857,52858,52859,52860,52861,52862,52863,52864,52865,52866,52867,52868,52869,52870,52871,52872,52873,52874,52875,52876,52877,52878,52879,52880,52881,52882,52883,52884,52885,52886,52887,52888,52889,52890,52891,52892,52893,52894,52895,52896,52897,52898,52899,52900,52901,52902,52903,52904,52905,52906,52907,52908,52909,52910,52911,52912,52913,52914,52915,52916,52917,52918,52919,52920,52921,52922,52923,52924,52925,52926,52927,52928,52929,52930,52931,52932,52933,52934,52935,52936,52937,52938,52939,52940,52941,52942,52943,52944,52945,52946,52947,52948,52949,52950,52951,52952,52953,52954,52955,52956,52957,52958,52959,52960,52961,52962,52963,52964,52965,52966,52967,52968,52969,52970,52971,52972,52973,52974,52975,52976,52977,52978,52979,52980,52981,52982,52983,52984,52985,52986,52987,52988,52989,52990,52991,52992,52993,52994,52995,52996,52997,52998,52999,53000,53001,53002,53003,53004,53005,53006,53007,53008,53009,53010,53011,53012,53013,53014,53015,53016,53017,53018,53019,53020,53021,53022,53023,53024,53025,53026,53027,53028,53029,53030,53031,53032,53033,53034,53035,53036,53037,53038,53039,53040,53041,53042,53043,53044,53045,53046,53047,53048,53049,53050,53051,53052,53053,53054,53055,53056,53057,53058,53059,53060,53061,53062,53063,53064,53065,53066,53067,53068,53069,53070,53071,53072,53073,53074,53075,53076,53077,53078,53079,53080,53081,53082,53083,53084,53085,53086,53087,53088,53089,53090,53091,53092,53093,53094,53095,53096,53097,53098,53099,53100,53101,53102,53103,53104,53105,53106,53107,53108,53109,53110,53111,53112,53113,53114,53115,53116,53117,53118,53119,53120,53121,53122,53123,53124,53125,53126,53127,53128,53129,53130,53131,53132,53133,53134,53135,53136,53137,53138,53139,53140,53141,53142,53143,53144,53145,53146,53147,53148,53149,53150,53151,53152,53153,53154,53155,53156,53157,53158,53159,53160,53161,53162,53163,53164,53165,53166,53167,53168,53169,53170,53171,53172,53173,53174,53175,53176,53177,53178,53179,53180,53181,53182,53183,53184,53185,53186,53187,53188,53189,53190,53191,53192,53193,53194,53195,53196,53197,53198,53199,53200,53201,53202,53203,53204,53205,53206,53207,53208,53209,53210,53211,53212,53213,53214,53215,53216,53217,53218,53219,53220,53221,53222,53223,53224,53225,53226,53227,53228,53229,53230,53231,53232,53233,53234,53235,53236,53237,53238,53239,53240,53241,53242,53243,53244,53245,53246,53247,53248,53249,53250,53251,53252,53253,53254,53255,53256,53257,53258,53259,53260,53261,53262,53263,53264,53265,53266,53267,53268,53269,53270,53271,53272,53273,53274,53275,53276,53277,53278,53279,53280,53281,53282,53283,53284,53285,53286,53287,53288,53289,53290,53291,53292,53293,53294,53295,53296,53297,53298,53299,53300,53301,53302,53303,53304,53305,53306,53307,53308,53309,53310,53311,53312,53313,53314,53315,53316,53317,53318,53319,53320,53321,53322,53323,53324,53325,53326,53327,53328,53329,53330,53331,53332,53333,53334,53335,53336,53337,53338,53339,53340,53341,53342,53343,53344,53345,53346,53347,53348,53349,53350,53351,53352,53353,53354,53355,53356,53357,53358,53359,53360,53361,53362,53363,53364,53365,53366,53367,53368,53369,53370,53371,53372,53373,53374,53375,53376,53377,53378,53379,53380,53381,53382,53383,53384,53385,53386,53387,53388,53389,53390,53391,53392,53393,53394,53395,53396,53397,53398,53399,53400,53401,53402,53403,53404,53405,53406,53407,53408,53409,53410,53411,53412,53413,53414,53415,53416,53417,53418,53419,53420,53421,53422,53423,53424,53425,53426,53427,53428,53429,53430,53431,53432,53433,53434,53435,53436,53437,53438,53439,53440,53441,53442,53443,53444,53445,53446,53447,53448,53449,53450,53451,53452,53453,53454,53455,53456,53457,53458,53459,53460,53461,53462,53463,53464,53465,53466,53467,53468,53469,53470,53471,53472,53473,53474,53475,53476,53477,53478,53479,53480,53481,53482,53483,53484,53485,53486,53487,53488,53489,53490,53491,53492,53493,53494,53495,53496,53497,53498,53499,53500,53501,53502,53503,53504,53505,53506,53507,53508,53509,53510,53511,53512,53513,53514,53515,53516,53517,53518,53519,53520,53521,53522,53523,53524,53525,53526,53527,53528,53529,53530,53531,53532,53533,53534,53535,53536,53537,53538,53539,53540,53541,53542,53543,53544,53545,53546,53547,53548,53549,53550,53551,53552,53553,53554,53555,53556,53557,53558,53559,53560,53561,53562,53563,53564,53565,53566,53567,53568,53569,53570,53571,53572,53573,53574,53575,53576,53577,53578,53579,53580,53581,53582,53583,53584,53585,53586,53587,53588,53589,53590,53591,53592,53593,53594,53595,53596,53597,53598,53599,53600,53601,53602,53603,53604,53605,53606,53607,53608,53609,53610,53611,53612,53613,53614,53615,53616,53617,53618,53619,53620,53621,53622,53623,53624,53625,53626,53627,53628,53629,53630,53631,53632,53633,53634,53635,53636,53637,53638,53639,53640,53641,53642,53643,53644,53645,53646,53647,53648,53649,53650,53651,53652,53653,53654,53655,53656,53657,53658,53659,53660,53661,53662,53663,53664,53665,53666,53667,53668,53669,53670,53671,53672,53673,53674,53675,53676,53677,53678,53679,53680,53681,53682,53683,53684,53685,53686,53687,53688,53689,53690,53691,53692,53693,53694,53695,53696,53697,53698,53699,53700,53701,53702,53703,53704,53705,53706,53707,53708,53709,53710,53711,53712,53713,53714,53715,53716,53717,53718,53719,53720,53721,53722,53723,53724,53725,53726,53727,53728,53729,53730,53731,53732,53733,53734,53735,53736,53737,53738,53739,53740,53741,53742,53743,53744,53745,53746,53747,53748,53749,53750,53751,53752,53753,53754,53755,53756,53757,53758,53759,53760,53761,53762,53763,53764,53765,53766,53767,53768,53769,53770,53771,53772,53773,53774,53775,53776,53777,53778,53779,53780,53781,53782,53783,53784,53785,53786,53787,53788,53789,53790,53791,53792,53793,53794,53795,53796,53797,53798,53799,53800,53801,53802,53803,53804,53805,53806,53807,53808,53809,53810,53811,53812,53813,53814,53815,53816,53817,53818,53819,53820,53821,53822,53823,53824,53825,53826,53827,53828,53829,53830,53831,53832,53833,53834,53835,53836,53837,53838,53839,53840,53841,53842,53843,53844,53845,53846,53847,53848,53849,53850,53851,53852,53853,53854,53855,53856,53857,53858,53859,53860,53861,53862,53863,53864,53865,53866,53867,53868,53869,53870,53871,53872,53873,53874,53875,53876,53877,53878,53879,53880,53881,53882,53883,53884,53885,53886,53887,53888,53889,53890,53891,53892,53893,53894,53895,53896,53897,53898,53899,53900,53901,53902,53903,53904,53905,53906,53907,53908,53909,53910,53911,53912,53913,53914,53915,53916,53917,53918,53919,53920,53921,53922,53923,53924,53925,53926,53927,53928,53929,53930,53931,53932,53933,53934,53935,53936,53937,53938,53939,53940,53941,53942,53943,53944,53945,53946,53947,53948,53949,53950,53951,53952,53953,53954,53955,53956,53957,53958,53959,53960,53961,53962,53963,53964,53965,53966,53967,53968,53969,53970,53971,53972,53973,53974,53975,53976,53977,53978,53979,53980,53981,53982,53983,53984,53985,53986,53987,53988,53989,53990,53991,53992,53993,53994,53995,53996,53997,53998,53999,54000,54001,54002,54003,54004,54005,54006,54007,54008,54009,54010,54011,54012,54013,54014,54015,54016,54017,54018,54019,54020,54021,54022,54023,54024,54025,54026,54027,54028,54029,54030,54031,54032,54033,54034,54035,54036,54037,54038,54039,54040,54041,54042,54043,54044,54045,54046,54047,54048,54049,54050,54051,54052,54053,54054,54055,54056,54057,54058,54059,54060,54061,54062,54063,54064,54065,54066,54067,54068,54069,54070,54071,54072,54073,54074,54075,54076,54077,54078,54079,54080,54081,54082,54083,54084,54085,54086,54087,54088,54089,54090,54091,54092,54093,54094,54095,54096,54097,54098,54099,54100,54101,54102,54103,54104,54105,54106,54107,54108,54109,54110,54111,54112,54113,54114,54115,54116,54117,54118,54119,54120,54121,54122,54123,54124,54125,54126,54127,54128,54129,54130,54131,54132,54133,54134,54135,54136,54137,54138,54139,54140,54141,54142,54143,54144,54145,54146,54147,54148,54149,54150,54151,54152,54153,54154,54155,54156,54157,54158,54159,54160,54161,54162,54163,54164,54165,54166,54167,54168,54169,54170,54171,54172,54173,54174,54175,54176,54177,54178,54179,54180,54181,54182,54183,54184,54185,54186,54187,54188,54189,54190,54191,54192,54193,54194,54195,54196,54197,54198,54199,54200,54201,54202,54203,54204,54205,54206,54207,54208,54209,54210,54211,54212,54213,54214,54215,54216,54217,54218,54219,54220,54221,54222,54223,54224,54225,54226,54227,54228,54229,54230,54231,54232,54233,54234,54235,54236,54237,54238,54239,54240,54241,54242,54243,54244,54245,54246,54247,54248,54249,54250,54251,54252,54253,54254,54255,54256,54257,54258,54259,54260,54261,54262,54263,54264,54265,54266,54267,54268,54269,54270,54271,54272,54273,54274,54275,54276,54277,54278,54279,54280,54281,54282,54283,54284,54285,54286,54287,54288,54289,54290,54291,54292,54293,54294,54295,54296,54297,54298,54299,54300,54301,54302,54303,54304,54305,54306,54307,54308,54309,54310,54311,54312,54313,54314,54315,54316,54317,54318,54319,54320,54321,54322,54323,54324,54325,54326,54327,54328,54329,54330,54331,54332,54333,54334,54335,54336,54337,54338,54339,54340,54341,54342,54343,54344,54345,54346,54347,54348,54349,54350,54351,54352,54353,54354,54355,54356,54357,54358,54359,54360,54361,54362,54363,54364,54365,54366,54367,54368,54369,54370,54371,54372,54373,54374,54375,54376,54377,54378,54379,54380,54381,54382,54383,54384,54385,54386,54387,54388,54389,54390,54391,54392,54393,54394,54395,54396,54397,54398,54399,54400,54401,54402,54403,54404,54405,54406,54407,54408,54409,54410,54411,54412,54413,54414,54415,54416,54417,54418,54419,54420,54421,54422,54423,54424,54425,54426,54427,54428,54429,54430,54431,54432,54433,54434,54435,54436,54437,54438,54439,54440,54441,54442,54443,54444,54445,54446,54447,54448,54449,54450,54451,54452,54453,54454,54455,54456,54457,54458,54459,54460,54461,54462,54463,54464,54465,54466,54467,54468,54469,54470,54471,54472,54473,54474,54475,54476,54477,54478,54479,54480,54481,54482,54483,54484,54485,54486,54487,54488,54489,54490,54491,54492,54493,54494,54495,54496,54497,54498,54499,54500,54501,54502,54503,54504,54505,54506,54507,54508,54509,54510,54511,54512,54513,54514,54515,54516,54517,54518,54519,54520,54521,54522,54523,54524,54525,54526,54527,54528,54529,54530,54531,54532,54533,54534,54535,54536,54537,54538,54539,54540,54541,54542,54543,54544,54545,54546,54547,54548,54549,54550,54551,54552,54553,54554,54555,54556,54557,54558,54559,54560,54561,54562,54563,54564,54565,54566,54567,54568,54569,54570,54571,54572,54573,54574,54575,54576,54577,54578,54579,54580,54581,54582,54583,54584,54585,54586,54587,54588,54589,54590,54591,54592,54593,54594,54595,54596,54597,54598,54599,54600,54601,54602,54603,54604,54605,54606,54607,54608,54609,54610,54611,54612,54613,54614,54615,54616,54617,54618,54619,54620,54621,54622,54623,54624,54625,54626,54627,54628,54629,54630,54631,54632,54633,54634,54635,54636,54637,54638,54639,54640,54641,54642,54643,54644,54645,54646,54647,54648,54649,54650,54651,54652,54653,54654,54655,54656,54657,54658,54659,54660,54661,54662,54663,54664,54665,54666,54667,54668,54669,54670,54671,54672,54673,54674,54675,54676,54677,54678,54679,54680,54681,54682,54683,54684,54685,54686,54687,54688,54689,54690,54691,54692,54693,54694,54695,54696,54697,54698,54699,54700,54701,54702,54703,54704,54705,54706,54707,54708,54709,54710,54711,54712,54713,54714,54715,54716,54717,54718,54719,54720,54721,54722,54723,54724,54725,54726,54727,54728,54729,54730,54731,54732,54733,54734,54735,54736,54737,54738,54739,54740,54741,54742,54743,54744,54745,54746,54747,54748,54749,54750,54751,54752,54753,54754,54755,54756,54757,54758,54759,54760,54761,54762,54763,54764,54765,54766,54767,54768,54769,54770,54771,54772,54773,54774,54775,54776,54777,54778,54779,54780,54781,54782,54783,54784,54785,54786,54787,54788,54789,54790,54791,54792,54793,54794,54795,54796,54797,54798,54799,54800,54801,54802,54803,54804,54805,54806,54807,54808,54809,54810,54811,54812,54813,54814,54815,54816,54817,54818,54819,54820,54821,54822,54823,54824,54825,54826,54827,54828,54829,54830,54831,54832,54833,54834,54835,54836,54837,54838,54839,54840,54841,54842,54843,54844,54845,54846,54847,54848,54849,54850,54851,54852,54853,54854,54855,54856,54857,54858,54859,54860,54861,54862,54863,54864,54865,54866,54867,54868,54869,54870,54871,54872,54873,54874,54875,54876,54877,54878,54879,54880,54881,54882,54883,54884,54885,54886,54887,54888,54889,54890,54891,54892,54893,54894,54895,54896,54897,54898,54899,54900,54901,54902,54903,54904,54905,54906,54907,54908,54909,54910,54911,54912,54913,54914,54915,54916,54917,54918,54919,54920,54921,54922,54923,54924,54925,54926,54927,54928,54929,54930,54931,54932,54933,54934,54935,54936,54937,54938,54939,54940,54941,54942,54943,54944,54945,54946,54947,54948,54949,54950,54951,54952,54953,54954,54955,54956,54957,54958,54959,54960,54961,54962,54963,54964,54965,54966,54967,54968,54969,54970,54971,54972,54973,54974,54975,54976,54977,54978,54979,54980,54981,54982,54983,54984,54985,54986,54987,54988,54989,54990,54991,54992,54993,54994,54995,54996,54997,54998,54999,55000,55001,55002,55003,55004,55005,55006,55007,55008,55009,55010,55011,55012,55013,55014,55015,55016,55017,55018,55019,55020,55021,55022,55023,55024,55025,55026,55027,55028,55029,55030,55031,55032,55033,55034,55035,55036,55037,55038,55039,55040,55041,55042,55043,55044,55045,55046,55047,55048,55049,55050,55051,55052,55053,55054,55055,55056,55057,55058,55059,55060,55061,55062,55063,55064,55065,55066,55067,55068,55069,55070,55071,55072,55073,55074,55075,55076,55077,55078,55079,55080,55081,55082,55083,55084,55085,55086,55087,55088,55089,55090,55091,55092,55093,55094,55095,55096,55097,55098,55099,55100,55101,55102,55103,55104,55105,55106,55107,55108,55109,55110,55111,55112,55113,55114,55115,55116,55117,55118,55119,55120,55121,55122,55123,55124,55125,55126,55127,55128,55129,55130,55131,55132,55133,55134,55135,55136,55137,55138,55139,55140,55141,55142,55143,55144,55145,55146,55147,55148,55149,55150,55151,55152,55153,55154,55155,55156,55157,55158,55159,55160,55161,55162,55163,55164,55165,55166,55167,55168,55169,55170,55171,55172,55173,55174,55175,55176,55177,55178,55179,55180,55181,55182,55183,55184,55185,55186,55187,55188,55189,55190,55191,55192,55193,55194,55195,55196,55197,55198,55199,55200,55201,55202,55203,55216,55217,55218,55219,55220,55221,55222,55223,55224,55225,55226,55227,55228,55229,55230,55231,55232,55233,55234,55235,55236,55237,55238,55243,55244,55245,55246,55247,55248,55249,55250,55251,55252,55253,55254,55255,55256,55257,55258,55259,55260,55261,55262,55263,55264,55265,55266,55267,55268,55269,55270,55271,55272,55273,55274,55275,55276,55277,55278,55279,55280,55281,55282,55283,55284,55285,55286,55287,55288,55289,55290,55291,63744,63745,63746,63747,63748,63749,63750,63751,63752,63753,63754,63755,63756,63757,63758,63759,63760,63761,63762,63763,63764,63765,63766,63767,63768,63769,63770,63771,63772,63773,63774,63775,63776,63777,63778,63779,63780,63781,63782,63783,63784,63785,63786,63787,63788,63789,63790,63791,63792,63793,63794,63795,63796,63797,63798,63799,63800,63801,63802,63803,63804,63805,63806,63807,63808,63809,63810,63811,63812,63813,63814,63815,63816,63817,63818,63819,63820,63821,63822,63823,63824,63825,63826,63827,63828,63829,63830,63831,63832,63833,63834,63835,63836,63837,63838,63839,63840,63841,63842,63843,63844,63845,63846,63847,63848,63849,63850,63851,63852,63853,63854,63855,63856,63857,63858,63859,63860,63861,63862,63863,63864,63865,63866,63867,63868,63869,63870,63871,63872,63873,63874,63875,63876,63877,63878,63879,63880,63881,63882,63883,63884,63885,63886,63887,63888,63889,63890,63891,63892,63893,63894,63895,63896,63897,63898,63899,63900,63901,63902,63903,63904,63905,63906,63907,63908,63909,63910,63911,63912,63913,63914,63915,63916,63917,63918,63919,63920,63921,63922,63923,63924,63925,63926,63927,63928,63929,63930,63931,63932,63933,63934,63935,63936,63937,63938,63939,63940,63941,63942,63943,63944,63945,63946,63947,63948,63949,63950,63951,63952,63953,63954,63955,63956,63957,63958,63959,63960,63961,63962,63963,63964,63965,63966,63967,63968,63969,63970,63971,63972,63973,63974,63975,63976,63977,63978,63979,63980,63981,63982,63983,63984,63985,63986,63987,63988,63989,63990,63991,63992,63993,63994,63995,63996,63997,63998,63999,64000,64001,64002,64003,64004,64005,64006,64007,64008,64009,64010,64011,64012,64013,64014,64015,64016,64017,64018,64019,64020,64021,64022,64023,64024,64025,64026,64027,64028,64029,64030,64031,64032,64033,64034,64035,64036,64037,64038,64039,64040,64041,64042,64043,64044,64045,64046,64047,64048,64049,64050,64051,64052,64053,64054,64055,64056,64057,64058,64059,64060,64061,64062,64063,64064,64065,64066,64067,64068,64069,64070,64071,64072,64073,64074,64075,64076,64077,64078,64079,64080,64081,64082,64083,64084,64085,64086,64087,64088,64089,64090,64091,64092,64093,64094,64095,64096,64097,64098,64099,64100,64101,64102,64103,64104,64105,64106,64107,64108,64109,64112,64113,64114,64115,64116,64117,64118,64119,64120,64121,64122,64123,64124,64125,64126,64127,64128,64129,64130,64131,64132,64133,64134,64135,64136,64137,64138,64139,64140,64141,64142,64143,64144,64145,64146,64147,64148,64149,64150,64151,64152,64153,64154,64155,64156,64157,64158,64159,64160,64161,64162,64163,64164,64165,64166,64167,64168,64169,64170,64171,64172,64173,64174,64175,64176,64177,64178,64179,64180,64181,64182,64183,64184,64185,64186,64187,64188,64189,64190,64191,64192,64193,64194,64195,64196,64197,64198,64199,64200,64201,64202,64203,64204,64205,64206,64207,64208,64209,64210,64211,64212,64213,64214,64215,64216,64217,64256,64257,64258,64259,64260,64261,64262,64275,64276,64277,64278,64279,64285,64287,64288,64289,64290,64291,64292,64293,64294,64295,64296,64298,64299,64300,64301,64302,64303,64304,64305,64306,64307,64308,64309,64310,64312,64313,64314,64315,64316,64318,64320,64321,64323,64324,64326,64327,64328,64329,64330,64331,64332,64333,64334,64335,64336,64337,64338,64339,64340,64341,64342,64343,64344,64345,64346,64347,64348,64349,64350,64351,64352,64353,64354,64355,64356,64357,64358,64359,64360,64361,64362,64363,64364,64365,64366,64367,64368,64369,64370,64371,64372,64373,64374,64375,64376,64377,64378,64379,64380,64381,64382,64383,64384,64385,64386,64387,64388,64389,64390,64391,64392,64393,64394,64395,64396,64397,64398,64399,64400,64401,64402,64403,64404,64405,64406,64407,64408,64409,64410,64411,64412,64413,64414,64415,64416,64417,64418,64419,64420,64421,64422,64423,64424,64425,64426,64427,64428,64429,64430,64431,64432,64433,64467,64468,64469,64470,64471,64472,64473,64474,64475,64476,64477,64478,64479,64480,64481,64482,64483,64484,64485,64486,64487,64488,64489,64490,64491,64492,64493,64494,64495,64496,64497,64498,64499,64500,64501,64502,64503,64504,64505,64506,64507,64508,64509,64510,64511,64512,64513,64514,64515,64516,64517,64518,64519,64520,64521,64522,64523,64524,64525,64526,64527,64528,64529,64530,64531,64532,64533,64534,64535,64536,64537,64538,64539,64540,64541,64542,64543,64544,64545,64546,64547,64548,64549,64550,64551,64552,64553,64554,64555,64556,64557,64558,64559,64560,64561,64562,64563,64564,64565,64566,64567,64568,64569,64570,64571,64572,64573,64574,64575,64576,64577,64578,64579,64580,64581,64582,64583,64584,64585,64586,64587,64588,64589,64590,64591,64592,64593,64594,64595,64596,64597,64598,64599,64600,64601,64602,64603,64604,64605,64606,64607,64608,64609,64610,64611,64612,64613,64614,64615,64616,64617,64618,64619,64620,64621,64622,64623,64624,64625,64626,64627,64628,64629,64630,64631,64632,64633,64634,64635,64636,64637,64638,64639,64640,64641,64642,64643,64644,64645,64646,64647,64648,64649,64650,64651,64652,64653,64654,64655,64656,64657,64658,64659,64660,64661,64662,64663,64664,64665,64666,64667,64668,64669,64670,64671,64672,64673,64674,64675,64676,64677,64678,64679,64680,64681,64682,64683,64684,64685,64686,64687,64688,64689,64690,64691,64692,64693,64694,64695,64696,64697,64698,64699,64700,64701,64702,64703,64704,64705,64706,64707,64708,64709,64710,64711,64712,64713,64714,64715,64716,64717,64718,64719,64720,64721,64722,64723,64724,64725,64726,64727,64728,64729,64730,64731,64732,64733,64734,64735,64736,64737,64738,64739,64740,64741,64742,64743,64744,64745,64746,64747,64748,64749,64750,64751,64752,64753,64754,64755,64756,64757,64758,64759,64760,64761,64762,64763,64764,64765,64766,64767,64768,64769,64770,64771,64772,64773,64774,64775,64776,64777,64778,64779,64780,64781,64782,64783,64784,64785,64786,64787,64788,64789,64790,64791,64792,64793,64794,64795,64796,64797,64798,64799,64800,64801,64802,64803,64804,64805,64806,64807,64808,64809,64810,64811,64812,64813,64814,64815,64816,64817,64818,64819,64820,64821,64822,64823,64824,64825,64826,64827,64828,64829,64848,64849,64850,64851,64852,64853,64854,64855,64856,64857,64858,64859,64860,64861,64862,64863,64864,64865,64866,64867,64868,64869,64870,64871,64872,64873,64874,64875,64876,64877,64878,64879,64880,64881,64882,64883,64884,64885,64886,64887,64888,64889,64890,64891,64892,64893,64894,64895,64896,64897,64898,64899,64900,64901,64902,64903,64904,64905,64906,64907,64908,64909,64910,64911,64914,64915,64916,64917,64918,64919,64920,64921,64922,64923,64924,64925,64926,64927,64928,64929,64930,64931,64932,64933,64934,64935,64936,64937,64938,64939,64940,64941,64942,64943,64944,64945,64946,64947,64948,64949,64950,64951,64952,64953,64954,64955,64956,64957,64958,64959,64960,64961,64962,64963,64964,64965,64966,64967,65008,65009,65010,65011,65012,65013,65014,65015,65016,65017,65018,65019,65136,65137,65138,65139,65140,65142,65143,65144,65145,65146,65147,65148,65149,65150,65151,65152,65153,65154,65155,65156,65157,65158,65159,65160,65161,65162,65163,65164,65165,65166,65167,65168,65169,65170,65171,65172,65173,65174,65175,65176,65177,65178,65179,65180,65181,65182,65183,65184,65185,65186,65187,65188,65189,65190,65191,65192,65193,65194,65195,65196,65197,65198,65199,65200,65201,65202,65203,65204,65205,65206,65207,65208,65209,65210,65211,65212,65213,65214,65215,65216,65217,65218,65219,65220,65221,65222,65223,65224,65225,65226,65227,65228,65229,65230,65231,65232,65233,65234,65235,65236,65237,65238,65239,65240,65241,65242,65243,65244,65245,65246,65247,65248,65249,65250,65251,65252,65253,65254,65255,65256,65257,65258,65259,65260,65261,65262,65263,65264,65265,65266,65267,65268,65269,65270,65271,65272,65273,65274,65275,65276,65313,65314,65315,65316,65317,65318,65319,65320,65321,65322,65323,65324,65325,65326,65327,65328,65329,65330,65331,65332,65333,65334,65335,65336,65337,65338,65345,65346,65347,65348,65349,65350,65351,65352,65353,65354,65355,65356,65357,65358,65359,65360,65361,65362,65363,65364,65365,65366,65367,65368,65369,65370,65382,65383,65384,65385,65386,65387,65388,65389,65390,65391,65392,65393,65394,65395,65396,65397,65398,65399,65400,65401,65402,65403,65404,65405,65406,65407,65408,65409,65410,65411,65412,65413,65414,65415,65416,65417,65418,65419,65420,65421,65422,65423,65424,65425,65426,65427,65428,65429,65430,65431,65432,65433,65434,65435,65436,65437,65438,65439,65440,65441,65442,65443,65444,65445,65446,65447,65448,65449,65450,65451,65452,65453,65454,65455,65456,65457,65458,65459,65460,65461,65462,65463,65464,65465,65466,65467,65468,65469,65470,65474,65475,65476,65477,65478,65479,65482,65483,65484,65485,65486,65487,65490,65491,65492,65493,65494,65495,65498,65499,65500'; var arr = str.split(',').map(function(code) { return parseInt(code, 10); }); module.exports = arr; },{}],5:[function(require,module,exports){ // http://wiki.commonjs.org/wiki/Unit_Testing/1.0 // // THIS IS NOT TESTED NOR LIKELY TO WORK OUTSIDE V8! // // Originally from narwhal.js (http://narwhaljs.org) // Copyright (c) 2009 Thomas Robinson <280north.com> // // 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 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. // when used in node, this will actually load the util module we depend on // versus loading the builtin util module as happens otherwise // this is a bug in node module loading as far as I am concerned var util = require('util/'); var pSlice = Array.prototype.slice; var hasOwn = Object.prototype.hasOwnProperty; // 1. The assert module provides functions that throw // AssertionError's when particular conditions are not met. The // assert module must conform to the following interface. var assert = module.exports = ok; // 2. The AssertionError is defined in assert. // new assert.AssertionError({ message: message, // actual: actual, // expected: expected }) assert.AssertionError = function AssertionError(options) { this.name = 'AssertionError'; this.actual = options.actual; this.expected = options.expected; this.operator = options.operator; if (options.message) { this.message = options.message; this.generatedMessage = false; } else { this.message = getMessage(this); this.generatedMessage = true; } var stackStartFunction = options.stackStartFunction || fail; if (Error.captureStackTrace) { Error.captureStackTrace(this, stackStartFunction); } else { // non v8 browsers so we can have a stacktrace var err = new Error(); if (err.stack) { var out = err.stack; // try to strip useless frames var fn_name = stackStartFunction.name; var idx = out.indexOf('\n' + fn_name); if (idx >= 0) { // once we have located the function frame // we need to strip out everything before it (and its line) var next_line = out.indexOf('\n', idx + 1); out = out.substring(next_line + 1); } this.stack = out; } } }; // assert.AssertionError instanceof Error util.inherits(assert.AssertionError, Error); function replacer(key, value) { if (util.isUndefined(value)) { return '' + value; } if (util.isNumber(value) && !isFinite(value)) { return value.toString(); } if (util.isFunction(value) || util.isRegExp(value)) { return value.toString(); } return value; } function truncate(s, n) { if (util.isString(s)) { return s.length < n ? s : s.slice(0, n); } else { return s; } } function getMessage(self) { return truncate(JSON.stringify(self.actual, replacer), 128) + ' ' + self.operator + ' ' + truncate(JSON.stringify(self.expected, replacer), 128); } // At present only the three keys mentioned above are used and // understood by the spec. Implementations or sub modules can pass // other keys to the AssertionError's constructor - they will be // ignored. // 3. All of the following functions must throw an AssertionError // when a corresponding condition is not met, with a message that // may be undefined if not provided. All assertion methods provide // both the actual and expected values to the assertion error for // display purposes. function fail(actual, expected, message, operator, stackStartFunction) { throw new assert.AssertionError({ message: message, actual: actual, expected: expected, operator: operator, stackStartFunction: stackStartFunction }); } // EXTENSION! allows for well behaved errors defined elsewhere. assert.fail = fail; // 4. Pure assertion tests whether a value is truthy, as determined // by !!guard. // assert.ok(guard, message_opt); // This statement is equivalent to assert.equal(true, !!guard, // message_opt);. To test strictly for the value true, use // assert.strictEqual(true, guard, message_opt);. function ok(value, message) { if (!value) fail(value, true, message, '==', assert.ok); } assert.ok = ok; // 5. The equality assertion tests shallow, coercive equality with // ==. // assert.equal(actual, expected, message_opt); assert.equal = function equal(actual, expected, message) { if (actual != expected) fail(actual, expected, message, '==', assert.equal); }; // 6. The non-equality assertion tests for whether two objects are not equal // with != assert.notEqual(actual, expected, message_opt); assert.notEqual = function notEqual(actual, expected, message) { if (actual == expected) { fail(actual, expected, message, '!=', assert.notEqual); } }; // 7. The equivalence assertion tests a deep equality relation. // assert.deepEqual(actual, expected, message_opt); assert.deepEqual = function deepEqual(actual, expected, message) { if (!_deepEqual(actual, expected)) { fail(actual, expected, message, 'deepEqual', assert.deepEqual); } }; function _deepEqual(actual, expected) { // 7.1. All identical values are equivalent, as determined by ===. if (actual === expected) { return true; } else if (util.isBuffer(actual) && util.isBuffer(expected)) { if (actual.length != expected.length) return false; for (var i = 0; i < actual.length; i++) { if (actual[i] !== expected[i]) return false; } return true; // 7.2. If the expected value is a Date object, the actual value is // equivalent if it is also a Date object that refers to the same time. } else if (util.isDate(actual) && util.isDate(expected)) { return actual.getTime() === expected.getTime(); // 7.3 If the expected value is a RegExp object, the actual value is // equivalent if it is also a RegExp object with the same source and // properties (`global`, `multiline`, `lastIndex`, `ignoreCase`). } else if (util.isRegExp(actual) && util.isRegExp(expected)) { return actual.source === expected.source && actual.global === expected.global && actual.multiline === expected.multiline && actual.lastIndex === expected.lastIndex && actual.ignoreCase === expected.ignoreCase; // 7.4. Other pairs that do not both pass typeof value == 'object', // equivalence is determined by ==. } else if (!util.isObject(actual) && !util.isObject(expected)) { return actual == expected; // 7.5 For all other Object pairs, including Array objects, equivalence is // determined by having the same number of owned properties (as verified // with Object.prototype.hasOwnProperty.call), the same set of keys // (although not necessarily the same order), equivalent values for every // corresponding key, and an identical 'prototype' property. Note: this // accounts for both named and indexed properties on Arrays. } else { return objEquiv(actual, expected); } } function isArguments(object) { return Object.prototype.toString.call(object) == '[object Arguments]'; } function objEquiv(a, b) { if (util.isNullOrUndefined(a) || util.isNullOrUndefined(b)) return false; // an identical 'prototype' property. if (a.prototype !== b.prototype) return false; // if one is a primitive, the other must be same if (util.isPrimitive(a) || util.isPrimitive(b)) { return a === b; } var aIsArgs = isArguments(a), bIsArgs = isArguments(b); if ((aIsArgs && !bIsArgs) || (!aIsArgs && bIsArgs)) return false; if (aIsArgs) { a = pSlice.call(a); b = pSlice.call(b); return _deepEqual(a, b); } var ka = objectKeys(a), kb = objectKeys(b), key, i; // having the same number of owned properties (keys incorporates // hasOwnProperty) if (ka.length != kb.length) return false; //the same set of keys (although not necessarily the same order), ka.sort(); kb.sort(); //~~~cheap key test for (i = ka.length - 1; i >= 0; i--) { if (ka[i] != kb[i]) return false; } //equivalent values for every corresponding key, and //~~~possibly expensive deep test for (i = ka.length - 1; i >= 0; i--) { key = ka[i]; if (!_deepEqual(a[key], b[key])) return false; } return true; } // 8. The non-equivalence assertion tests for any deep inequality. // assert.notDeepEqual(actual, expected, message_opt); assert.notDeepEqual = function notDeepEqual(actual, expected, message) { if (_deepEqual(actual, expected)) { fail(actual, expected, message, 'notDeepEqual', assert.notDeepEqual); } }; // 9. The strict equality assertion tests strict equality, as determined by ===. // assert.strictEqual(actual, expected, message_opt); assert.strictEqual = function strictEqual(actual, expected, message) { if (actual !== expected) { fail(actual, expected, message, '===', assert.strictEqual); } }; // 10. The strict non-equality assertion tests for strict inequality, as // determined by !==. assert.notStrictEqual(actual, expected, message_opt); assert.notStrictEqual = function notStrictEqual(actual, expected, message) { if (actual === expected) { fail(actual, expected, message, '!==', assert.notStrictEqual); } }; function expectedException(actual, expected) { if (!actual || !expected) { return false; } if (Object.prototype.toString.call(expected) == '[object RegExp]') { return expected.test(actual); } else if (actual instanceof expected) { return true; } else if (expected.call({}, actual) === true) { return true; } return false; } function _throws(shouldThrow, block, expected, message) { var actual; if (util.isString(expected)) { message = expected; expected = null; } try { block(); } catch (e) { actual = e; } message = (expected && expected.name ? ' (' + expected.name + ').' : '.') + (message ? ' ' + message : '.'); if (shouldThrow && !actual) { fail(actual, expected, 'Missing expected exception' + message); } if (!shouldThrow && expectedException(actual, expected)) { fail(actual, expected, 'Got unwanted exception' + message); } if ((shouldThrow && actual && expected && !expectedException(actual, expected)) || (!shouldThrow && actual)) { throw actual; } } // 11. Expected to throw an error: // assert.throws(block, Error_opt, message_opt); assert.throws = function(block, /*optional*/error, /*optional*/message) { _throws.apply(this, [true].concat(pSlice.call(arguments))); }; // EXTENSION! This is annoying to write outside this module. assert.doesNotThrow = function(block, /*optional*/message) { _throws.apply(this, [false].concat(pSlice.call(arguments))); }; assert.ifError = function(err) { if (err) {throw err;}}; var objectKeys = Object.keys || function (obj) { var keys = []; for (var key in obj) { if (hasOwn.call(obj, key)) keys.push(key); } return keys; }; },{"util/":8}],6:[function(require,module,exports){ if (typeof Object.create === 'function') { // implementation from standard node.js 'util' module module.exports = function inherits(ctor, superCtor) { ctor.super_ = superCtor ctor.prototype = Object.create(superCtor.prototype, { constructor: { value: ctor, enumerable: false, writable: true, configurable: true } }); }; } else { // old school shim for old browsers module.exports = function inherits(ctor, superCtor) { ctor.super_ = superCtor var TempCtor = function () {} TempCtor.prototype = superCtor.prototype ctor.prototype = new TempCtor() ctor.prototype.constructor = ctor } } },{}],7:[function(require,module,exports){ module.exports = function isBuffer(arg) { return arg && typeof arg === 'object' && typeof arg.copy === 'function' && typeof arg.fill === 'function' && typeof arg.readUInt8 === 'function'; } },{}],8:[function(require,module,exports){ (function (process,global){ // Copyright Joyent, Inc. and other Node contributors. // // 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. var formatRegExp = /%[sdj%]/g; exports.format = function(f) { if (!isString(f)) { var objects = []; for (var i = 0; i < arguments.length; i++) { objects.push(inspect(arguments[i])); } return objects.join(' '); } var i = 1; var args = arguments; var len = args.length; var str = String(f).replace(formatRegExp, function(x) { if (x === '%%') return '%'; if (i >= len) return x; switch (x) { case '%s': return String(args[i++]); case '%d': return Number(args[i++]); case '%j': try { return JSON.stringify(args[i++]); } catch (_) { return '[Circular]'; } default: return x; } }); for (var x = args[i]; i < len; x = args[++i]) { if (isNull(x) || !isObject(x)) { str += ' ' + x; } else { str += ' ' + inspect(x); } } return str; }; // Mark that a method should not be used. // Returns a modified function which warns once by default. // If --no-deprecation is set, then it is a no-op. exports.deprecate = function(fn, msg) { // Allow for deprecating things in the process of starting up. if (isUndefined(global.process)) { return function() { return exports.deprecate(fn, msg).apply(this, arguments); }; } if (process.noDeprecation === true) { return fn; } var warned = false; function deprecated() { if (!warned) { if (process.throwDeprecation) { throw new Error(msg); } else if (process.traceDeprecation) { console.trace(msg); } else { console.error(msg); } warned = true; } return fn.apply(this, arguments); } return deprecated; }; var debugs = {}; var debugEnviron; exports.debuglog = function(set) { if (isUndefined(debugEnviron)) debugEnviron = process.env.NODE_DEBUG || ''; set = set.toUpperCase(); if (!debugs[set]) { if (new RegExp('\\b' + set + '\\b', 'i').test(debugEnviron)) { var pid = process.pid; debugs[set] = function() { var msg = exports.format.apply(exports, arguments); console.error('%s %d: %s', set, pid, msg); }; } else { debugs[set] = function() {}; } } return debugs[set]; }; /** * Echos the value of a value. Trys to print the value out * in the best way possible given the different types. * * @param {Object} obj The object to print out. * @param {Object} opts Optional options object that alters the output. */ /* legacy: obj, showHidden, depth, colors*/ function inspect(obj, opts) { // default options var ctx = { seen: [], stylize: stylizeNoColor }; // legacy... if (arguments.length >= 3) ctx.depth = arguments[2]; if (arguments.length >= 4) ctx.colors = arguments[3]; if (isBoolean(opts)) { // legacy... ctx.showHidden = opts; } else if (opts) { // got an "options" object exports._extend(ctx, opts); } // set default options if (isUndefined(ctx.showHidden)) ctx.showHidden = false; if (isUndefined(ctx.depth)) ctx.depth = 2; if (isUndefined(ctx.colors)) ctx.colors = false; if (isUndefined(ctx.customInspect)) ctx.customInspect = true; if (ctx.colors) ctx.stylize = stylizeWithColor; return formatValue(ctx, obj, ctx.depth); } exports.inspect = inspect; // http://en.wikipedia.org/wiki/ANSI_escape_code#graphics inspect.colors = { 'bold' : [1, 22], 'italic' : [3, 23], 'underline' : [4, 24], 'inverse' : [7, 27], 'white' : [37, 39], 'grey' : [90, 39], 'black' : [30, 39], 'blue' : [34, 39], 'cyan' : [36, 39], 'green' : [32, 39], 'magenta' : [35, 39], 'red' : [31, 39], 'yellow' : [33, 39] }; // Don't use 'blue' not visible on cmd.exe inspect.styles = { 'special': 'cyan', 'number': 'yellow', 'boolean': 'yellow', 'undefined': 'grey', 'null': 'bold', 'string': 'green', 'date': 'magenta', // "name": intentionally not styling 'regexp': 'red' }; function stylizeWithColor(str, styleType) { var style = inspect.styles[styleType]; if (style) { return '\u001b[' + inspect.colors[style][0] + 'm' + str + '\u001b[' + inspect.colors[style][1] + 'm'; } else { return str; } } function stylizeNoColor(str, styleType) { return str; } function arrayToHash(array) { var hash = {}; array.forEach(function(val, idx) { hash[val] = true; }); return hash; } function formatValue(ctx, value, recurseTimes) { // Provide a hook for user-specified inspect functions. // Check that value is an object with an inspect function on it if (ctx.customInspect && value && isFunction(value.inspect) && // Filter out the util module, it's inspect function is special value.inspect !== exports.inspect && // Also filter out any prototype objects using the circular check. !(value.constructor && value.constructor.prototype === value)) { var ret = value.inspect(recurseTimes, ctx); if (!isString(ret)) { ret = formatValue(ctx, ret, recurseTimes); } return ret; } // Primitive types cannot have properties var primitive = formatPrimitive(ctx, value); if (primitive) { return primitive; } // Look up the keys of the object. var keys = Object.keys(value); var visibleKeys = arrayToHash(keys); if (ctx.showHidden) { keys = Object.getOwnPropertyNames(value); } // IE doesn't make error fields non-enumerable // http://msdn.microsoft.com/en-us/library/ie/dww52sbt(v=vs.94).aspx if (isError(value) && (keys.indexOf('message') >= 0 || keys.indexOf('description') >= 0)) { return formatError(value); } // Some type of object without properties can be shortcutted. if (keys.length === 0) { if (isFunction(value)) { var name = value.name ? ': ' + value.name : ''; return ctx.stylize('[Function' + name + ']', 'special'); } if (isRegExp(value)) { return ctx.stylize(RegExp.prototype.toString.call(value), 'regexp'); } if (isDate(value)) { return ctx.stylize(Date.prototype.toString.call(value), 'date'); } if (isError(value)) { return formatError(value); } } var base = '', array = false, braces = ['{', '}']; // Make Array say that they are Array if (isArray(value)) { array = true; braces = ['[', ']']; } // Make functions say that they are functions if (isFunction(value)) { var n = value.name ? ': ' + value.name : ''; base = ' [Function' + n + ']'; } // Make RegExps say that they are RegExps if (isRegExp(value)) { base = ' ' + RegExp.prototype.toString.call(value); } // Make dates with properties first say the date if (isDate(value)) { base = ' ' + Date.prototype.toUTCString.call(value); } // Make error with message first say the error if (isError(value)) { base = ' ' + formatError(value); } if (keys.length === 0 && (!array || value.length == 0)) { return braces[0] + base + braces[1]; } if (recurseTimes < 0) { if (isRegExp(value)) { return ctx.stylize(RegExp.prototype.toString.call(value), 'regexp'); } else { return ctx.stylize('[Object]', 'special'); } } ctx.seen.push(value); var output; if (array) { output = formatArray(ctx, value, recurseTimes, visibleKeys, keys); } else { output = keys.map(function(key) { return formatProperty(ctx, value, recurseTimes, visibleKeys, key, array); }); } ctx.seen.pop(); return reduceToSingleString(output, base, braces); } function formatPrimitive(ctx, value) { if (isUndefined(value)) return ctx.stylize('undefined', 'undefined'); if (isString(value)) { var simple = '\'' + JSON.stringify(value).replace(/^"|"$/g, '') .replace(/'/g, "\\'") .replace(/\\"/g, '"') + '\''; return ctx.stylize(simple, 'string'); } if (isNumber(value)) return ctx.stylize('' + value, 'number'); if (isBoolean(value)) return ctx.stylize('' + value, 'boolean'); // For some reason typeof null is "object", so special case here. if (isNull(value)) return ctx.stylize('null', 'null'); } function formatError(value) { return '[' + Error.prototype.toString.call(value) + ']'; } function formatArray(ctx, value, recurseTimes, visibleKeys, keys) { var output = []; for (var i = 0, l = value.length; i < l; ++i) { if (hasOwnProperty(value, String(i))) { output.push(formatProperty(ctx, value, recurseTimes, visibleKeys, String(i), true)); } else { output.push(''); } } keys.forEach(function(key) { if (!key.match(/^\d+$/)) { output.push(formatProperty(ctx, value, recurseTimes, visibleKeys, key, true)); } }); return output; } function formatProperty(ctx, value, recurseTimes, visibleKeys, key, array) { var name, str, desc; desc = Object.getOwnPropertyDescriptor(value, key) || { value: value[key] }; if (desc.get) { if (desc.set) { str = ctx.stylize('[Getter/Setter]', 'special'); } else { str = ctx.stylize('[Getter]', 'special'); } } else { if (desc.set) { str = ctx.stylize('[Setter]', 'special'); } } if (!hasOwnProperty(visibleKeys, key)) { name = '[' + key + ']'; } if (!str) { if (ctx.seen.indexOf(desc.value) < 0) { if (isNull(recurseTimes)) { str = formatValue(ctx, desc.value, null); } else { str = formatValue(ctx, desc.value, recurseTimes - 1); } if (str.indexOf('\n') > -1) { if (array) { str = str.split('\n').map(function(line) { return ' ' + line; }).join('\n').substr(2); } else { str = '\n' + str.split('\n').map(function(line) { return ' ' + line; }).join('\n'); } } } else { str = ctx.stylize('[Circular]', 'special'); } } if (isUndefined(name)) { if (array && key.match(/^\d+$/)) { return str; } name = JSON.stringify('' + key); if (name.match(/^"([a-zA-Z_][a-zA-Z_0-9]*)"$/)) { name = name.substr(1, name.length - 2); name = ctx.stylize(name, 'name'); } else { name = name.replace(/'/g, "\\'") .replace(/\\"/g, '"') .replace(/(^"|"$)/g, "'"); name = ctx.stylize(name, 'string'); } } return name + ': ' + str; } function reduceToSingleString(output, base, braces) { var numLinesEst = 0; var length = output.reduce(function(prev, cur) { numLinesEst++; if (cur.indexOf('\n') >= 0) numLinesEst++; return prev + cur.replace(/\u001b\[\d\d?m/g, '').length + 1; }, 0); if (length > 60) { return braces[0] + (base === '' ? '' : base + '\n ') + ' ' + output.join(',\n ') + ' ' + braces[1]; } return braces[0] + base + ' ' + output.join(', ') + ' ' + braces[1]; } // NOTE: These type checking functions intentionally don't use `instanceof` // because it is fragile and can be easily faked with `Object.create()`. function isArray(ar) { return Array.isArray(ar); } exports.isArray = isArray; function isBoolean(arg) { return typeof arg === 'boolean'; } exports.isBoolean = isBoolean; function isNull(arg) { return arg === null; } exports.isNull = isNull; function isNullOrUndefined(arg) { return arg == null; } exports.isNullOrUndefined = isNullOrUndefined; function isNumber(arg) { return typeof arg === 'number'; } exports.isNumber = isNumber; function isString(arg) { return typeof arg === 'string'; } exports.isString = isString; function isSymbol(arg) { return typeof arg === 'symbol'; } exports.isSymbol = isSymbol; function isUndefined(arg) { return arg === void 0; } exports.isUndefined = isUndefined; function isRegExp(re) { return isObject(re) && objectToString(re) === '[object RegExp]'; } exports.isRegExp = isRegExp; function isObject(arg) { return typeof arg === 'object' && arg !== null; } exports.isObject = isObject; function isDate(d) { return isObject(d) && objectToString(d) === '[object Date]'; } exports.isDate = isDate; function isError(e) { return isObject(e) && (objectToString(e) === '[object Error]' || e instanceof Error); } exports.isError = isError; function isFunction(arg) { return typeof arg === 'function'; } exports.isFunction = isFunction; function isPrimitive(arg) { return arg === null || typeof arg === 'boolean' || typeof arg === 'number' || typeof arg === 'string' || typeof arg === 'symbol' || // ES6 symbol typeof arg === 'undefined'; } exports.isPrimitive = isPrimitive; exports.isBuffer = require('./support/isBuffer'); function objectToString(o) { return Object.prototype.toString.call(o); } function pad(n) { return n < 10 ? '0' + n.toString(10) : n.toString(10); } var months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; // 26 Feb 16:19:34 function timestamp() { var d = new Date(); var time = [pad(d.getHours()), pad(d.getMinutes()), pad(d.getSeconds())].join(':'); return [d.getDate(), months[d.getMonth()], time].join(' '); } // log is just a thin wrapper to console.log that prepends a timestamp exports.log = function() { console.log('%s - %s', timestamp(), exports.format.apply(exports, arguments)); }; /** * Inherit the prototype methods from one constructor into another. * * The Function.prototype.inherits from lang.js rewritten as a standalone * function (not on Function.prototype). NOTE: If this file is to be loaded * during bootstrapping this function needs to be rewritten using some native * functions as prototype setup using normal JavaScript does not work as * expected during bootstrapping (see mirror.js in r114903). * * @param {function} ctor Constructor function which needs to inherit the * prototype. * @param {function} superCtor Constructor function to inherit prototype from. */ exports.inherits = require('inherits'); exports._extend = function(origin, add) { // Don't do anything if add isn't an object if (!add || !isObject(add)) return origin; var keys = Object.keys(add); var i = keys.length; while (i--) { origin[keys[i]] = add[keys[i]]; } return origin; }; function hasOwnProperty(obj, prop) { return Object.prototype.hasOwnProperty.call(obj, prop); } }).call(this,require('_process'),typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}) },{"./support/isBuffer":7,"_process":13,"inherits":6}],9:[function(require,module,exports){ (function (global){ /*global window, global*/ var util = require("util") var assert = require("assert") var now = require("date-now") var slice = Array.prototype.slice var console var times = {} if (typeof global !== "undefined" && global.console) { console = global.console } else if (typeof window !== "undefined" && window.console) { console = window.console } else { console = {} } var functions = [ [log, "log"], [info, "info"], [warn, "warn"], [error, "error"], [time, "time"], [timeEnd, "timeEnd"], [trace, "trace"], [dir, "dir"], [consoleAssert, "assert"] ] for (var i = 0; i < functions.length; i++) { var tuple = functions[i] var f = tuple[0] var name = tuple[1] if (!console[name]) { console[name] = f } } module.exports = console function log() {} function info() { console.log.apply(console, arguments) } function warn() { console.log.apply(console, arguments) } function error() { console.warn.apply(console, arguments) } function time(label) { times[label] = now() } function timeEnd(label) { var time = times[label] if (!time) { throw new Error("No such label: " + label) } var duration = now() - time console.log(label + ": " + duration + "ms") } function trace() { var err = new Error() err.name = "Trace" err.message = util.format.apply(null, arguments) console.error(err.stack) } function dir(object) { console.log(util.inspect(object) + "\n") } function consoleAssert(expression) { if (!expression) { var arr = slice.call(arguments, 1) assert.ok(false, util.format.apply(null, arr)) } } }).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}) },{"assert":5,"date-now":10,"util":16}],10:[function(require,module,exports){ module.exports = now function now() { return new Date().getTime() } },{}],11:[function(require,module,exports){ // Copyright Joyent, Inc. and other Node contributors. // // 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. function EventEmitter() { this._events = this._events || {}; this._maxListeners = this._maxListeners || undefined; } module.exports = EventEmitter; // Backwards-compat with node 0.10.x EventEmitter.EventEmitter = EventEmitter; EventEmitter.prototype._events = undefined; EventEmitter.prototype._maxListeners = undefined; // By default EventEmitters will print a warning if more than 10 listeners are // added to it. This is a useful default which helps finding memory leaks. EventEmitter.defaultMaxListeners = 10; // Obviously not all Emitters should be limited to 10. This function allows // that to be increased. Set to zero for unlimited. EventEmitter.prototype.setMaxListeners = function(n) { if (!isNumber(n) || n < 0 || isNaN(n)) throw TypeError('n must be a positive number'); this._maxListeners = n; return this; }; EventEmitter.prototype.emit = function(type) { var er, handler, len, args, i, listeners; if (!this._events) this._events = {}; // If there is no 'error' event listener then throw. if (type === 'error') { if (!this._events.error || (isObject(this._events.error) && !this._events.error.length)) { er = arguments[1]; if (er instanceof Error) { throw er; // Unhandled 'error' event } throw TypeError('Uncaught, unspecified "error" event.'); } } handler = this._events[type]; if (isUndefined(handler)) return false; if (isFunction(handler)) { switch (arguments.length) { // fast cases case 1: handler.call(this); break; case 2: handler.call(this, arguments[1]); break; case 3: handler.call(this, arguments[1], arguments[2]); break; // slower default: len = arguments.length; args = new Array(len - 1); for (i = 1; i < len; i++) args[i - 1] = arguments[i]; handler.apply(this, args); } } else if (isObject(handler)) { len = arguments.length; args = new Array(len - 1); for (i = 1; i < len; i++) args[i - 1] = arguments[i]; listeners = handler.slice(); len = listeners.length; for (i = 0; i < len; i++) listeners[i].apply(this, args); } return true; }; EventEmitter.prototype.addListener = function(type, listener) { var m; if (!isFunction(listener)) throw TypeError('listener must be a function'); if (!this._events) this._events = {}; // To avoid recursion in the case that type === "newListener"! Before // adding it to the listeners, first emit "newListener". if (this._events.newListener) this.emit('newListener', type, isFunction(listener.listener) ? listener.listener : listener); if (!this._events[type]) // Optimize the case of one listener. Don't need the extra array object. this._events[type] = listener; else if (isObject(this._events[type])) // If we've already got an array, just append. this._events[type].push(listener); else // Adding the second element, need to change to array. this._events[type] = [this._events[type], listener]; // Check for listener leak if (isObject(this._events[type]) && !this._events[type].warned) { var m; if (!isUndefined(this._maxListeners)) { m = this._maxListeners; } else { m = EventEmitter.defaultMaxListeners; } if (m && m > 0 && this._events[type].length > m) { this._events[type].warned = true; console.error('(node) warning: possible EventEmitter memory ' + 'leak detected. %d listeners added. ' + 'Use emitter.setMaxListeners() to increase limit.', this._events[type].length); if (typeof console.trace === 'function') { // not supported in IE 10 console.trace(); } } } return this; }; EventEmitter.prototype.on = EventEmitter.prototype.addListener; EventEmitter.prototype.once = function(type, listener) { if (!isFunction(listener)) throw TypeError('listener must be a function'); var fired = false; function g() { this.removeListener(type, g); if (!fired) { fired = true; listener.apply(this, arguments); } } g.listener = listener; this.on(type, g); return this; }; // emits a 'removeListener' event iff the listener was removed EventEmitter.prototype.removeListener = function(type, listener) { var list, position, length, i; if (!isFunction(listener)) throw TypeError('listener must be a function'); if (!this._events || !this._events[type]) return this; list = this._events[type]; length = list.length; position = -1; if (list === listener || (isFunction(list.listener) && list.listener === listener)) { delete this._events[type]; if (this._events.removeListener) this.emit('removeListener', type, listener); } else if (isObject(list)) { for (i = length; i-- > 0;) { if (list[i] === listener || (list[i].listener && list[i].listener === listener)) { position = i; break; } } if (position < 0) return this; if (list.length === 1) { list.length = 0; delete this._events[type]; } else { list.splice(position, 1); } if (this._events.removeListener) this.emit('removeListener', type, listener); } return this; }; EventEmitter.prototype.removeAllListeners = function(type) { var key, listeners; if (!this._events) return this; // not listening for removeListener, no need to emit if (!this._events.removeListener) { if (arguments.length === 0) this._events = {}; else if (this._events[type]) delete this._events[type]; return this; } // emit removeListener for all listeners on all events if (arguments.length === 0) { for (key in this._events) { if (key === 'removeListener') continue; this.removeAllListeners(key); } this.removeAllListeners('removeListener'); this._events = {}; return this; } listeners = this._events[type]; if (isFunction(listeners)) { this.removeListener(type, listeners); } else { // LIFO order while (listeners.length) this.removeListener(type, listeners[listeners.length - 1]); } delete this._events[type]; return this; }; EventEmitter.prototype.listeners = function(type) { var ret; if (!this._events || !this._events[type]) ret = []; else if (isFunction(this._events[type])) ret = [this._events[type]]; else ret = this._events[type].slice(); return ret; }; EventEmitter.listenerCount = function(emitter, type) { var ret; if (!emitter._events || !emitter._events[type]) ret = 0; else if (isFunction(emitter._events[type])) ret = 1; else ret = emitter._events[type].length; return ret; }; function isFunction(arg) { return typeof arg === 'function'; } function isNumber(arg) { return typeof arg === 'number'; } function isObject(arg) { return typeof arg === 'object' && arg !== null; } function isUndefined(arg) { return arg === void 0; } },{}],12:[function(require,module,exports){ (function (global){ /** * @license * Lodash * Copyright OpenJS Foundation and other contributors * Released under MIT license * Based on Underscore.js 1.8.3 * Copyright Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors */ ;(function() { /** Used as a safe reference for `undefined` in pre-ES5 environments. */ var undefined; /** Used as the semantic version number. */ var VERSION = '4.17.20'; /** Used as the size to enable large array optimizations. */ var LARGE_ARRAY_SIZE = 200; /** Error message constants. */ var CORE_ERROR_TEXT = 'Unsupported core-js use. Try https://npms.io/search?q=ponyfill.', FUNC_ERROR_TEXT = 'Expected a function'; /** Used to stand-in for `undefined` hash values. */ var HASH_UNDEFINED = '__lodash_hash_undefined__'; /** Used as the maximum memoize cache size. */ var MAX_MEMOIZE_SIZE = 500; /** Used as the internal argument placeholder. */ var PLACEHOLDER = '__lodash_placeholder__'; /** Used to compose bitmasks for cloning. */ var CLONE_DEEP_FLAG = 1, CLONE_FLAT_FLAG = 2, CLONE_SYMBOLS_FLAG = 4; /** Used to compose bitmasks for value comparisons. */ var COMPARE_PARTIAL_FLAG = 1, COMPARE_UNORDERED_FLAG = 2; /** Used to compose bitmasks for function metadata. */ var WRAP_BIND_FLAG = 1, WRAP_BIND_KEY_FLAG = 2, WRAP_CURRY_BOUND_FLAG = 4, WRAP_CURRY_FLAG = 8, WRAP_CURRY_RIGHT_FLAG = 16, WRAP_PARTIAL_FLAG = 32, WRAP_PARTIAL_RIGHT_FLAG = 64, WRAP_ARY_FLAG = 128, WRAP_REARG_FLAG = 256, WRAP_FLIP_FLAG = 512; /** Used as default options for `_.truncate`. */ var DEFAULT_TRUNC_LENGTH = 30, DEFAULT_TRUNC_OMISSION = '...'; /** Used to detect hot functions by number of calls within a span of milliseconds. */ var HOT_COUNT = 800, HOT_SPAN = 16; /** Used to indicate the type of lazy iteratees. */ var LAZY_FILTER_FLAG = 1, LAZY_MAP_FLAG = 2, LAZY_WHILE_FLAG = 3; /** Used as references for various `Number` constants. */ var INFINITY = 1 / 0, MAX_SAFE_INTEGER = 9007199254740991, MAX_INTEGER = 1.7976931348623157e+308, NAN = 0 / 0; /** Used as references for the maximum length and index of an array. */ var MAX_ARRAY_LENGTH = 4294967295, MAX_ARRAY_INDEX = MAX_ARRAY_LENGTH - 1, HALF_MAX_ARRAY_LENGTH = MAX_ARRAY_LENGTH >>> 1; /** Used to associate wrap methods with their bit flags. */ var wrapFlags = [ ['ary', WRAP_ARY_FLAG], ['bind', WRAP_BIND_FLAG], ['bindKey', WRAP_BIND_KEY_FLAG], ['curry', WRAP_CURRY_FLAG], ['curryRight', WRAP_CURRY_RIGHT_FLAG], ['flip', WRAP_FLIP_FLAG], ['partial', WRAP_PARTIAL_FLAG], ['partialRight', WRAP_PARTIAL_RIGHT_FLAG], ['rearg', WRAP_REARG_FLAG] ]; /** `Object#toString` result references. */ var argsTag = '[object Arguments]', arrayTag = '[object Array]', asyncTag = '[object AsyncFunction]', boolTag = '[object Boolean]', dateTag = '[object Date]', domExcTag = '[object DOMException]', errorTag = '[object Error]', funcTag = '[object Function]', genTag = '[object GeneratorFunction]', mapTag = '[object Map]', numberTag = '[object Number]', nullTag = '[object Null]', objectTag = '[object Object]', promiseTag = '[object Promise]', proxyTag = '[object Proxy]', regexpTag = '[object RegExp]', setTag = '[object Set]', stringTag = '[object String]', symbolTag = '[object Symbol]', undefinedTag = '[object Undefined]', weakMapTag = '[object WeakMap]', weakSetTag = '[object WeakSet]'; var arrayBufferTag = '[object ArrayBuffer]', dataViewTag = '[object DataView]', float32Tag = '[object Float32Array]', float64Tag = '[object Float64Array]', int8Tag = '[object Int8Array]', int16Tag = '[object Int16Array]', int32Tag = '[object Int32Array]', uint8Tag = '[object Uint8Array]', uint8ClampedTag = '[object Uint8ClampedArray]', uint16Tag = '[object Uint16Array]', uint32Tag = '[object Uint32Array]'; /** Used to match empty string literals in compiled template source. */ var reEmptyStringLeading = /\b__p \+= '';/g, reEmptyStringMiddle = /\b(__p \+=) '' \+/g, reEmptyStringTrailing = /(__e\(.*?\)|\b__t\)) \+\n'';/g; /** Used to match HTML entities and HTML characters. */ var reEscapedHtml = /&(?:amp|lt|gt|quot|#39);/g, reUnescapedHtml = /[&<>"']/g, reHasEscapedHtml = RegExp(reEscapedHtml.source), reHasUnescapedHtml = RegExp(reUnescapedHtml.source); /** Used to match template delimiters. */ var reEscape = /<%-([\s\S]+?)%>/g, reEvaluate = /<%([\s\S]+?)%>/g, reInterpolate = /<%=([\s\S]+?)%>/g; /** Used to match property names within property paths. */ var reIsDeepProp = /\.|\[(?:[^[\]]*|(["'])(?:(?!\1)[^\\]|\\.)*?\1)\]/, reIsPlainProp = /^\w*$/, rePropName = /[^.[\]]+|\[(?:(-?\d+(?:\.\d+)?)|(["'])((?:(?!\2)[^\\]|\\.)*?)\2)\]|(?=(?:\.|\[\])(?:\.|\[\]|$))/g; /** * Used to match `RegExp` * [syntax characters](http://ecma-international.org/ecma-262/7.0/#sec-patterns). */ var reRegExpChar = /[\\^$.*+?()[\]{}|]/g, reHasRegExpChar = RegExp(reRegExpChar.source); /** Used to match leading and trailing whitespace. */ var reTrim = /^\s+|\s+$/g, reTrimStart = /^\s+/, reTrimEnd = /\s+$/; /** Used to match wrap detail comments. */ var reWrapComment = /\{(?:\n\/\* \[wrapped with .+\] \*\/)?\n?/, reWrapDetails = /\{\n\/\* \[wrapped with (.+)\] \*/, reSplitDetails = /,? & /; /** Used to match words composed of alphanumeric characters. */ var reAsciiWord = /[^\x00-\x2f\x3a-\x40\x5b-\x60\x7b-\x7f]+/g; /** Used to match backslashes in property paths. */ var reEscapeChar = /\\(\\)?/g; /** * Used to match * [ES template delimiters](http://ecma-international.org/ecma-262/7.0/#sec-template-literal-lexical-components). */ var reEsTemplate = /\$\{([^\\}]*(?:\\.[^\\}]*)*)\}/g; /** Used to match `RegExp` flags from their coerced string values. */ var reFlags = /\w*$/; /** Used to detect bad signed hexadecimal string values. */ var reIsBadHex = /^[-+]0x[0-9a-f]+$/i; /** Used to detect binary string values. */ var reIsBinary = /^0b[01]+$/i; /** Used to detect host constructors (Safari). */ var reIsHostCtor = /^\[object .+?Constructor\]$/; /** Used to detect octal string values. */ var reIsOctal = /^0o[0-7]+$/i; /** Used to detect unsigned integer values. */ var reIsUint = /^(?:0|[1-9]\d*)$/; /** Used to match Latin Unicode letters (excluding mathematical operators). */ var reLatin = /[\xc0-\xd6\xd8-\xf6\xf8-\xff\u0100-\u017f]/g; /** Used to ensure capturing order of template delimiters. */ var reNoMatch = /($^)/; /** Used to match unescaped characters in compiled string literals. */ var reUnescapedString = /['\n\r\u2028\u2029\\]/g; /** Used to compose unicode character classes. */ var rsAstralRange = '\\ud800-\\udfff', rsComboMarksRange = '\\u0300-\\u036f', reComboHalfMarksRange = '\\ufe20-\\ufe2f', rsComboSymbolsRange = '\\u20d0-\\u20ff', rsComboRange = rsComboMarksRange + reComboHalfMarksRange + rsComboSymbolsRange, rsDingbatRange = '\\u2700-\\u27bf', rsLowerRange = 'a-z\\xdf-\\xf6\\xf8-\\xff', rsMathOpRange = '\\xac\\xb1\\xd7\\xf7', rsNonCharRange = '\\x00-\\x2f\\x3a-\\x40\\x5b-\\x60\\x7b-\\xbf', rsPunctuationRange = '\\u2000-\\u206f', rsSpaceRange = ' \\t\\x0b\\f\\xa0\\ufeff\\n\\r\\u2028\\u2029\\u1680\\u180e\\u2000\\u2001\\u2002\\u2003\\u2004\\u2005\\u2006\\u2007\\u2008\\u2009\\u200a\\u202f\\u205f\\u3000', rsUpperRange = 'A-Z\\xc0-\\xd6\\xd8-\\xde', rsVarRange = '\\ufe0e\\ufe0f', rsBreakRange = rsMathOpRange + rsNonCharRange + rsPunctuationRange + rsSpaceRange; /** Used to compose unicode capture groups. */ var rsApos = "['\u2019]", rsAstral = '[' + rsAstralRange + ']', rsBreak = '[' + rsBreakRange + ']', rsCombo = '[' + rsComboRange + ']', rsDigits = '\\d+', rsDingbat = '[' + rsDingbatRange + ']', rsLower = '[' + rsLowerRange + ']', rsMisc = '[^' + rsAstralRange + rsBreakRange + rsDigits + rsDingbatRange + rsLowerRange + rsUpperRange + ']', rsFitz = '\\ud83c[\\udffb-\\udfff]', rsModifier = '(?:' + rsCombo + '|' + rsFitz + ')', rsNonAstral = '[^' + rsAstralRange + ']', rsRegional = '(?:\\ud83c[\\udde6-\\uddff]){2}', rsSurrPair = '[\\ud800-\\udbff][\\udc00-\\udfff]', rsUpper = '[' + rsUpperRange + ']', rsZWJ = '\\u200d'; /** Used to compose unicode regexes. */ var rsMiscLower = '(?:' + rsLower + '|' + rsMisc + ')', rsMiscUpper = '(?:' + rsUpper + '|' + rsMisc + ')', rsOptContrLower = '(?:' + rsApos + '(?:d|ll|m|re|s|t|ve))?', rsOptContrUpper = '(?:' + rsApos + '(?:D|LL|M|RE|S|T|VE))?', reOptMod = rsModifier + '?', rsOptVar = '[' + rsVarRange + ']?', rsOptJoin = '(?:' + rsZWJ + '(?:' + [rsNonAstral, rsRegional, rsSurrPair].join('|') + ')' + rsOptVar + reOptMod + ')*', rsOrdLower = '\\d*(?:1st|2nd|3rd|(?![123])\\dth)(?=\\b|[A-Z_])', rsOrdUpper = '\\d*(?:1ST|2ND|3RD|(?![123])\\dTH)(?=\\b|[a-z_])', rsSeq = rsOptVar + reOptMod + rsOptJoin, rsEmoji = '(?:' + [rsDingbat, rsRegional, rsSurrPair].join('|') + ')' + rsSeq, rsSymbol = '(?:' + [rsNonAstral + rsCombo + '?', rsCombo, rsRegional, rsSurrPair, rsAstral].join('|') + ')'; /** Used to match apostrophes. */ var reApos = RegExp(rsApos, 'g'); /** * Used to match [combining diacritical marks](https://en.wikipedia.org/wiki/Combining_Diacritical_Marks) and * [combining diacritical marks for symbols](https://en.wikipedia.org/wiki/Combining_Diacritical_Marks_for_Symbols). */ var reComboMark = RegExp(rsCombo, 'g'); /** Used to match [string symbols](https://mathiasbynens.be/notes/javascript-unicode). */ var reUnicode = RegExp(rsFitz + '(?=' + rsFitz + ')|' + rsSymbol + rsSeq, 'g'); /** Used to match complex or compound words. */ var reUnicodeWord = RegExp([ rsUpper + '?' + rsLower + '+' + rsOptContrLower + '(?=' + [rsBreak, rsUpper, '$'].join('|') + ')', rsMiscUpper + '+' + rsOptContrUpper + '(?=' + [rsBreak, rsUpper + rsMiscLower, '$'].join('|') + ')', rsUpper + '?' + rsMiscLower + '+' + rsOptContrLower, rsUpper + '+' + rsOptContrUpper, rsOrdUpper, rsOrdLower, rsDigits, rsEmoji ].join('|'), 'g'); /** Used to detect strings with [zero-width joiners or code points from the astral planes](http://eev.ee/blog/2015/09/12/dark-corners-of-unicode/). */ var reHasUnicode = RegExp('[' + rsZWJ + rsAstralRange + rsComboRange + rsVarRange + ']'); /** Used to detect strings that need a more robust regexp to match words. */ var reHasUnicodeWord = /[a-z][A-Z]|[A-Z]{2}[a-z]|[0-9][a-zA-Z]|[a-zA-Z][0-9]|[^a-zA-Z0-9 ]/; /** Used to assign default `context` object properties. */ var contextProps = [ 'Array', 'Buffer', 'DataView', 'Date', 'Error', 'Float32Array', 'Float64Array', 'Function', 'Int8Array', 'Int16Array', 'Int32Array', 'Map', 'Math', 'Object', 'Promise', 'RegExp', 'Set', 'String', 'Symbol', 'TypeError', 'Uint8Array', 'Uint8ClampedArray', 'Uint16Array', 'Uint32Array', 'WeakMap', '_', 'clearTimeout', 'isFinite', 'parseInt', 'setTimeout' ]; /** Used to make template sourceURLs easier to identify. */ var templateCounter = -1; /** Used to identify `toStringTag` values of typed arrays. */ var typedArrayTags = {}; typedArrayTags[float32Tag] = typedArrayTags[float64Tag] = typedArrayTags[int8Tag] = typedArrayTags[int16Tag] = typedArrayTags[int32Tag] = typedArrayTags[uint8Tag] = typedArrayTags[uint8ClampedTag] = typedArrayTags[uint16Tag] = typedArrayTags[uint32Tag] = true; typedArrayTags[argsTag] = typedArrayTags[arrayTag] = typedArrayTags[arrayBufferTag] = typedArrayTags[boolTag] = typedArrayTags[dataViewTag] = typedArrayTags[dateTag] = typedArrayTags[errorTag] = typedArrayTags[funcTag] = typedArrayTags[mapTag] = typedArrayTags[numberTag] = typedArrayTags[objectTag] = typedArrayTags[regexpTag] = typedArrayTags[setTag] = typedArrayTags[stringTag] = typedArrayTags[weakMapTag] = false; /** Used to identify `toStringTag` values supported by `_.clone`. */ var cloneableTags = {}; cloneableTags[argsTag] = cloneableTags[arrayTag] = cloneableTags[arrayBufferTag] = cloneableTags[dataViewTag] = cloneableTags[boolTag] = cloneableTags[dateTag] = cloneableTags[float32Tag] = cloneableTags[float64Tag] = cloneableTags[int8Tag] = cloneableTags[int16Tag] = cloneableTags[int32Tag] = cloneableTags[mapTag] = cloneableTags[numberTag] = cloneableTags[objectTag] = cloneableTags[regexpTag] = cloneableTags[setTag] = cloneableTags[stringTag] = cloneableTags[symbolTag] = cloneableTags[uint8Tag] = cloneableTags[uint8ClampedTag] = cloneableTags[uint16Tag] = cloneableTags[uint32Tag] = true; cloneableTags[errorTag] = cloneableTags[funcTag] = cloneableTags[weakMapTag] = false; /** Used to map Latin Unicode letters to basic Latin letters. */ var deburredLetters = { // Latin-1 Supplement block. '\xc0': 'A', '\xc1': 'A', '\xc2': 'A', '\xc3': 'A', '\xc4': 'A', '\xc5': 'A', '\xe0': 'a', '\xe1': 'a', '\xe2': 'a', '\xe3': 'a', '\xe4': 'a', '\xe5': 'a', '\xc7': 'C', '\xe7': 'c', '\xd0': 'D', '\xf0': 'd', '\xc8': 'E', '\xc9': 'E', '\xca': 'E', '\xcb': 'E', '\xe8': 'e', '\xe9': 'e', '\xea': 'e', '\xeb': 'e', '\xcc': 'I', '\xcd': 'I', '\xce': 'I', '\xcf': 'I', '\xec': 'i', '\xed': 'i', '\xee': 'i', '\xef': 'i', '\xd1': 'N', '\xf1': 'n', '\xd2': 'O', '\xd3': 'O', '\xd4': 'O', '\xd5': 'O', '\xd6': 'O', '\xd8': 'O', '\xf2': 'o', '\xf3': 'o', '\xf4': 'o', '\xf5': 'o', '\xf6': 'o', '\xf8': 'o', '\xd9': 'U', '\xda': 'U', '\xdb': 'U', '\xdc': 'U', '\xf9': 'u', '\xfa': 'u', '\xfb': 'u', '\xfc': 'u', '\xdd': 'Y', '\xfd': 'y', '\xff': 'y', '\xc6': 'Ae', '\xe6': 'ae', '\xde': 'Th', '\xfe': 'th', '\xdf': 'ss', // Latin Extended-A block. '\u0100': 'A', '\u0102': 'A', '\u0104': 'A', '\u0101': 'a', '\u0103': 'a', '\u0105': 'a', '\u0106': 'C', '\u0108': 'C', '\u010a': 'C', '\u010c': 'C', '\u0107': 'c', '\u0109': 'c', '\u010b': 'c', '\u010d': 'c', '\u010e': 'D', '\u0110': 'D', '\u010f': 'd', '\u0111': 'd', '\u0112': 'E', '\u0114': 'E', '\u0116': 'E', '\u0118': 'E', '\u011a': 'E', '\u0113': 'e', '\u0115': 'e', '\u0117': 'e', '\u0119': 'e', '\u011b': 'e', '\u011c': 'G', '\u011e': 'G', '\u0120': 'G', '\u0122': 'G', '\u011d': 'g', '\u011f': 'g', '\u0121': 'g', '\u0123': 'g', '\u0124': 'H', '\u0126': 'H', '\u0125': 'h', '\u0127': 'h', '\u0128': 'I', '\u012a': 'I', '\u012c': 'I', '\u012e': 'I', '\u0130': 'I', '\u0129': 'i', '\u012b': 'i', '\u012d': 'i', '\u012f': 'i', '\u0131': 'i', '\u0134': 'J', '\u0135': 'j', '\u0136': 'K', '\u0137': 'k', '\u0138': 'k', '\u0139': 'L', '\u013b': 'L', '\u013d': 'L', '\u013f': 'L', '\u0141': 'L', '\u013a': 'l', '\u013c': 'l', '\u013e': 'l', '\u0140': 'l', '\u0142': 'l', '\u0143': 'N', '\u0145': 'N', '\u0147': 'N', '\u014a': 'N', '\u0144': 'n', '\u0146': 'n', '\u0148': 'n', '\u014b': 'n', '\u014c': 'O', '\u014e': 'O', '\u0150': 'O', '\u014d': 'o', '\u014f': 'o', '\u0151': 'o', '\u0154': 'R', '\u0156': 'R', '\u0158': 'R', '\u0155': 'r', '\u0157': 'r', '\u0159': 'r', '\u015a': 'S', '\u015c': 'S', '\u015e': 'S', '\u0160': 'S', '\u015b': 's', '\u015d': 's', '\u015f': 's', '\u0161': 's', '\u0162': 'T', '\u0164': 'T', '\u0166': 'T', '\u0163': 't', '\u0165': 't', '\u0167': 't', '\u0168': 'U', '\u016a': 'U', '\u016c': 'U', '\u016e': 'U', '\u0170': 'U', '\u0172': 'U', '\u0169': 'u', '\u016b': 'u', '\u016d': 'u', '\u016f': 'u', '\u0171': 'u', '\u0173': 'u', '\u0174': 'W', '\u0175': 'w', '\u0176': 'Y', '\u0177': 'y', '\u0178': 'Y', '\u0179': 'Z', '\u017b': 'Z', '\u017d': 'Z', '\u017a': 'z', '\u017c': 'z', '\u017e': 'z', '\u0132': 'IJ', '\u0133': 'ij', '\u0152': 'Oe', '\u0153': 'oe', '\u0149': "'n", '\u017f': 's' }; /** Used to map characters to HTML entities. */ var htmlEscapes = { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }; /** Used to map HTML entities to characters. */ var htmlUnescapes = { '&': '&', '<': '<', '>': '>', '"': '"', ''': "'" }; /** Used to escape characters for inclusion in compiled string literals. */ var stringEscapes = { '\\': '\\', "'": "'", '\n': 'n', '\r': 'r', '\u2028': 'u2028', '\u2029': 'u2029' }; /** Built-in method references without a dependency on `root`. */ var freeParseFloat = parseFloat, freeParseInt = parseInt; /** Detect free variable `global` from Node.js. */ var freeGlobal = typeof global == 'object' && global && global.Object === Object && global; /** Detect free variable `self`. */ var freeSelf = typeof self == 'object' && self && self.Object === Object && self; /** Used as a reference to the global object. */ var root = freeGlobal || freeSelf || Function('return this')(); /** Detect free variable `exports`. */ var freeExports = typeof exports == 'object' && exports && !exports.nodeType && exports; /** Detect free variable `module`. */ var freeModule = freeExports && typeof module == 'object' && module && !module.nodeType && module; /** Detect the popular CommonJS extension `module.exports`. */ var moduleExports = freeModule && freeModule.exports === freeExports; /** Detect free variable `process` from Node.js. */ var freeProcess = moduleExports && freeGlobal.process; /** Used to access faster Node.js helpers. */ var nodeUtil = (function() { try { // Use `util.types` for Node.js 10+. var types = freeModule && freeModule.require && freeModule.require('util').types; if (types) { return types; } // Legacy `process.binding('util')` for Node.js < 10. return freeProcess && freeProcess.binding && freeProcess.binding('util'); } catch (e) {} }()); /* Node.js helper references. */ var nodeIsArrayBuffer = nodeUtil && nodeUtil.isArrayBuffer, nodeIsDate = nodeUtil && nodeUtil.isDate, nodeIsMap = nodeUtil && nodeUtil.isMap, nodeIsRegExp = nodeUtil && nodeUtil.isRegExp, nodeIsSet = nodeUtil && nodeUtil.isSet, nodeIsTypedArray = nodeUtil && nodeUtil.isTypedArray; /*--------------------------------------------------------------------------*/ /** * A faster alternative to `Function#apply`, this function invokes `func` * with the `this` binding of `thisArg` and the arguments of `args`. * * @private * @param {Function} func The function to invoke. * @param {*} thisArg The `this` binding of `func`. * @param {Array} args The arguments to invoke `func` with. * @returns {*} Returns the result of `func`. */ function apply(func, thisArg, args) { switch (args.length) { case 0: return func.call(thisArg); case 1: return func.call(thisArg, args[0]); case 2: return func.call(thisArg, args[0], args[1]); case 3: return func.call(thisArg, args[0], args[1], args[2]); } return func.apply(thisArg, args); } /** * A specialized version of `baseAggregator` for arrays. * * @private * @param {Array} [array] The array to iterate over. * @param {Function} setter The function to set `accumulator` values. * @param {Function} iteratee The iteratee to transform keys. * @param {Object} accumulator The initial aggregated object. * @returns {Function} Returns `accumulator`. */ function arrayAggregator(array, setter, iteratee, accumulator) { var index = -1, length = array == null ? 0 : array.length; while (++index < length) { var value = array[index]; setter(accumulator, value, iteratee(value), array); } return accumulator; } /** * A specialized version of `_.forEach` for arrays without support for * iteratee shorthands. * * @private * @param {Array} [array] The array to iterate over. * @param {Function} iteratee The function invoked per iteration. * @returns {Array} Returns `array`. */ function arrayEach(array, iteratee) { var index = -1, length = array == null ? 0 : array.length; while (++index < length) { if (iteratee(array[index], index, array) === false) { break; } } return array; } /** * A specialized version of `_.forEachRight` for arrays without support for * iteratee shorthands. * * @private * @param {Array} [array] The array to iterate over. * @param {Function} iteratee The function invoked per iteration. * @returns {Array} Returns `array`. */ function arrayEachRight(array, iteratee) { var length = array == null ? 0 : array.length; while (length--) { if (iteratee(array[length], length, array) === false) { break; } } return array; } /** * A specialized version of `_.every` for arrays without support for * iteratee shorthands. * * @private * @param {Array} [array] The array to iterate over. * @param {Function} predicate The function invoked per iteration. * @returns {boolean} Returns `true` if all elements pass the predicate check, * else `false`. */ function arrayEvery(array, predicate) { var index = -1, length = array == null ? 0 : array.length; while (++index < length) { if (!predicate(array[index], index, array)) { return false; } } return true; } /** * A specialized version of `_.filter` for arrays without support for * iteratee shorthands. * * @private * @param {Array} [array] The array to iterate over. * @param {Function} predicate The function invoked per iteration. * @returns {Array} Returns the new filtered array. */ function arrayFilter(array, predicate) { var index = -1, length = array == null ? 0 : array.length, resIndex = 0, result = []; while (++index < length) { var value = array[index]; if (predicate(value, index, array)) { result[resIndex++] = value; } } return result; } /** * A specialized version of `_.includes` for arrays without support for * specifying an index to search from. * * @private * @param {Array} [array] The array to inspect. * @param {*} target The value to search for. * @returns {boolean} Returns `true` if `target` is found, else `false`. */ function arrayIncludes(array, value) { var length = array == null ? 0 : array.length; return !!length && baseIndexOf(array, value, 0) > -1; } /** * This function is like `arrayIncludes` except that it accepts a comparator. * * @private * @param {Array} [array] The array to inspect. * @param {*} target The value to search for. * @param {Function} comparator The comparator invoked per element. * @returns {boolean} Returns `true` if `target` is found, else `false`. */ function arrayIncludesWith(array, value, comparator) { var index = -1, length = array == null ? 0 : array.length; while (++index < length) { if (comparator(value, array[index])) { return true; } } return false; } /** * A specialized version of `_.map` for arrays without support for iteratee * shorthands. * * @private * @param {Array} [array] The array to iterate over. * @param {Function} iteratee The function invoked per iteration. * @returns {Array} Returns the new mapped array. */ function arrayMap(array, iteratee) { var index = -1, length = array == null ? 0 : array.length, result = Array(length); while (++index < length) { result[index] = iteratee(array[index], index, array); } return result; } /** * Appends the elements of `values` to `array`. * * @private * @param {Array} array The array to modify. * @param {Array} values The values to append. * @returns {Array} Returns `array`. */ function arrayPush(array, values) { var index = -1, length = values.length, offset = array.length; while (++index < length) { array[offset + index] = values[index]; } return array; } /** * A specialized version of `_.reduce` for arrays without support for * iteratee shorthands. * * @private * @param {Array} [array] The array to iterate over. * @param {Function} iteratee The function invoked per iteration. * @param {*} [accumulator] The initial value. * @param {boolean} [initAccum] Specify using the first element of `array` as * the initial value. * @returns {*} Returns the accumulated value. */ function arrayReduce(array, iteratee, accumulator, initAccum) { var index = -1, length = array == null ? 0 : array.length; if (initAccum && length) { accumulator = array[++index]; } while (++index < length) { accumulator = iteratee(accumulator, array[index], index, array); } return accumulator; } /** * A specialized version of `_.reduceRight` for arrays without support for * iteratee shorthands. * * @private * @param {Array} [array] The array to iterate over. * @param {Function} iteratee The function invoked per iteration. * @param {*} [accumulator] The initial value. * @param {boolean} [initAccum] Specify using the last element of `array` as * the initial value. * @returns {*} Returns the accumulated value. */ function arrayReduceRight(array, iteratee, accumulator, initAccum) { var length = array == null ? 0 : array.length; if (initAccum && length) { accumulator = array[--length]; } while (length--) { accumulator = iteratee(accumulator, array[length], length, array); } return accumulator; } /** * A specialized version of `_.some` for arrays without support for iteratee * shorthands. * * @private * @param {Array} [array] The array to iterate over. * @param {Function} predicate The function invoked per iteration. * @returns {boolean} Returns `true` if any element passes the predicate check, * else `false`. */ function arraySome(array, predicate) { var index = -1, length = array == null ? 0 : array.length; while (++index < length) { if (predicate(array[index], index, array)) { return true; } } return false; } /** * Gets the size of an ASCII `string`. * * @private * @param {string} string The string inspect. * @returns {number} Returns the string size. */ var asciiSize = baseProperty('length'); /** * Converts an ASCII `string` to an array. * * @private * @param {string} string The string to convert. * @returns {Array} Returns the converted array. */ function asciiToArray(string) { return string.split(''); } /** * Splits an ASCII `string` into an array of its words. * * @private * @param {string} The string to inspect. * @returns {Array} Returns the words of `string`. */ function asciiWords(string) { return string.match(reAsciiWord) || []; } /** * The base implementation of methods like `_.findKey` and `_.findLastKey`, * without support for iteratee shorthands, which iterates over `collection` * using `eachFunc`. * * @private * @param {Array|Object} collection The collection to inspect. * @param {Function} predicate The function invoked per iteration. * @param {Function} eachFunc The function to iterate over `collection`. * @returns {*} Returns the found element or its key, else `undefined`. */ function baseFindKey(collection, predicate, eachFunc) { var result; eachFunc(collection, function(value, key, collection) { if (predicate(value, key, collection)) { result = key; return false; } }); return result; } /** * The base implementation of `_.findIndex` and `_.findLastIndex` without * support for iteratee shorthands. * * @private * @param {Array} array The array to inspect. * @param {Function} predicate The function invoked per iteration. * @param {number} fromIndex The index to search from. * @param {boolean} [fromRight] Specify iterating from right to left. * @returns {number} Returns the index of the matched value, else `-1`. */ function baseFindIndex(array, predicate, fromIndex, fromRight) { var length = array.length, index = fromIndex + (fromRight ? 1 : -1); while ((fromRight ? index-- : ++index < length)) { if (predicate(array[index], index, array)) { return index; } } return -1; } /** * The base implementation of `_.indexOf` without `fromIndex` bounds checks. * * @private * @param {Array} array The array to inspect. * @param {*} value The value to search for. * @param {number} fromIndex The index to search from. * @returns {number} Returns the index of the matched value, else `-1`. */ function baseIndexOf(array, value, fromIndex) { return value === value ? strictIndexOf(array, value, fromIndex) : baseFindIndex(array, baseIsNaN, fromIndex); } /** * This function is like `baseIndexOf` except that it accepts a comparator. * * @private * @param {Array} array The array to inspect. * @param {*} value The value to search for. * @param {number} fromIndex The index to search from. * @param {Function} comparator The comparator invoked per element. * @returns {number} Returns the index of the matched value, else `-1`. */ function baseIndexOfWith(array, value, fromIndex, comparator) { var index = fromIndex - 1, length = array.length; while (++index < length) { if (comparator(array[index], value)) { return index; } } return -1; } /** * The base implementation of `_.isNaN` without support for number objects. * * @private * @param {*} value The value to check. * @returns {boolean} Returns `true` if `value` is `NaN`, else `false`. */ function baseIsNaN(value) { return value !== value; } /** * The base implementation of `_.mean` and `_.meanBy` without support for * iteratee shorthands. * * @private * @param {Array} array The array to iterate over. * @param {Function} iteratee The function invoked per iteration. * @returns {number} Returns the mean. */ function baseMean(array, iteratee) { var length = array == null ? 0 : array.length; return length ? (baseSum(array, iteratee) / length) : NAN; } /** * The base implementation of `_.property` without support for deep paths. * * @private * @param {string} key The key of the property to get. * @returns {Function} Returns the new accessor function. */ function baseProperty(key) { return function(object) { return object == null ? undefined : object[key]; }; } /** * The base implementation of `_.propertyOf` without support for deep paths. * * @private * @param {Object} object The object to query. * @returns {Function} Returns the new accessor function. */ function basePropertyOf(object) { return function(key) { return object == null ? undefined : object[key]; }; } /** * The base implementation of `_.reduce` and `_.reduceRight`, without support * for iteratee shorthands, which iterates over `collection` using `eachFunc`. * * @private * @param {Array|Object} collection The collection to iterate over. * @param {Function} iteratee The function invoked per iteration. * @param {*} accumulator The initial value. * @param {boolean} initAccum Specify using the first or last element of * `collection` as the initial value. * @param {Function} eachFunc The function to iterate over `collection`. * @returns {*} Returns the accumulated value. */ function baseReduce(collection, iteratee, accumulator, initAccum, eachFunc) { eachFunc(collection, function(value, index, collection) { accumulator = initAccum ? (initAccum = false, value) : iteratee(accumulator, value, index, collection); }); return accumulator; } /** * The base implementation of `_.sortBy` which uses `comparer` to define the * sort order of `array` and replaces criteria objects with their corresponding * values. * * @private * @param {Array} array The array to sort. * @param {Function} comparer The function to define sort order. * @returns {Array} Returns `array`. */ function baseSortBy(array, comparer) { var length = array.length; array.sort(comparer); while (length--) { array[length] = array[length].value; } return array; } /** * The base implementation of `_.sum` and `_.sumBy` without support for * iteratee shorthands. * * @private * @param {Array} array The array to iterate over. * @param {Function} iteratee The function invoked per iteration. * @returns {number} Returns the sum. */ function baseSum(array, iteratee) { var result, index = -1, length = array.length; while (++index < length) { var current = iteratee(array[index]); if (current !== undefined) { result = result === undefined ? current : (result + current); } } return result; } /** * The base implementation of `_.times` without support for iteratee shorthands * or max array length checks. * * @private * @param {number} n The number of times to invoke `iteratee`. * @param {Function} iteratee The function invoked per iteration. * @returns {Array} Returns the array of results. */ function baseTimes(n, iteratee) { var index = -1, result = Array(n); while (++index < n) { result[index] = iteratee(index); } return result; } /** * The base implementation of `_.toPairs` and `_.toPairsIn` which creates an array * of key-value pairs for `object` corresponding to the property names of `props`. * * @private * @param {Object} object The object to query. * @param {Array} props The property names to get values for. * @returns {Object} Returns the key-value pairs. */ function baseToPairs(object, props) { return arrayMap(props, function(key) { return [key, object[key]]; }); } /** * The base implementation of `_.unary` without support for storing metadata. * * @private * @param {Function} func The function to cap arguments for. * @returns {Function} Returns the new capped function. */ function baseUnary(func) { return function(value) { return func(value); }; } /** * The base implementation of `_.values` and `_.valuesIn` which creates an * array of `object` property values corresponding to the property names * of `props`. * * @private * @param {Object} object The object to query. * @param {Array} props The property names to get values for. * @returns {Object} Returns the array of property values. */ function baseValues(object, props) { return arrayMap(props, function(key) { return object[key]; }); } /** * Checks if a `cache` value for `key` exists. * * @private * @param {Object} cache The cache to query. * @param {string} key The key of the entry to check. * @returns {boolean} Returns `true` if an entry for `key` exists, else `false`. */ function cacheHas(cache, key) { return cache.has(key); } /** * Used by `_.trim` and `_.trimStart` to get the index of the first string symbol * that is not found in the character symbols. * * @private * @param {Array} strSymbols The string symbols to inspect. * @param {Array} chrSymbols The character symbols to find. * @returns {number} Returns the index of the first unmatched string symbol. */ function charsStartIndex(strSymbols, chrSymbols) { var index = -1, length = strSymbols.length; while (++index < length && baseIndexOf(chrSymbols, strSymbols[index], 0) > -1) {} return index; } /** * Used by `_.trim` and `_.trimEnd` to get the index of the last string symbol * that is not found in the character symbols. * * @private * @param {Array} strSymbols The string symbols to inspect. * @param {Array} chrSymbols The character symbols to find. * @returns {number} Returns the index of the last unmatched string symbol. */ function charsEndIndex(strSymbols, chrSymbols) { var index = strSymbols.length; while (index-- && baseIndexOf(chrSymbols, strSymbols[index], 0) > -1) {} return index; } /** * Gets the number of `placeholder` occurrences in `array`. * * @private * @param {Array} array The array to inspect. * @param {*} placeholder The placeholder to search for. * @returns {number} Returns the placeholder count. */ function countHolders(array, placeholder) { var length = array.length, result = 0; while (length--) { if (array[length] === placeholder) { ++result; } } return result; } /** * Used by `_.deburr` to convert Latin-1 Supplement and Latin Extended-A * letters to basic Latin letters. * * @private * @param {string} letter The matched letter to deburr. * @returns {string} Returns the deburred letter. */ var deburrLetter = basePropertyOf(deburredLetters); /** * Used by `_.escape` to convert characters to HTML entities. * * @private * @param {string} chr The matched character to escape. * @returns {string} Returns the escaped character. */ var escapeHtmlChar = basePropertyOf(htmlEscapes); /** * Used by `_.template` to escape characters for inclusion in compiled string literals. * * @private * @param {string} chr The matched character to escape. * @returns {string} Returns the escaped character. */ function escapeStringChar(chr) { return '\\' + stringEscapes[chr]; } /** * Gets the value at `key` of `object`. * * @private * @param {Object} [object] The object to query. * @param {string} key The key of the property to get. * @returns {*} Returns the property value. */ function getValue(object, key) { return object == null ? undefined : object[key]; } /** * Checks if `string` contains Unicode symbols. * * @private * @param {string} string The string to inspect. * @returns {boolean} Returns `true` if a symbol is found, else `false`. */ function hasUnicode(string) { return reHasUnicode.test(string); } /** * Checks if `string` contains a word composed of Unicode symbols. * * @private * @param {string} string The string to inspect. * @returns {boolean} Returns `true` if a word is found, else `false`. */ function hasUnicodeWord(string) { return reHasUnicodeWord.test(string); } /** * Converts `iterator` to an array. * * @private * @param {Object} iterator The iterator to convert. * @returns {Array} Returns the converted array. */ function iteratorToArray(iterator) { var data, result = []; while (!(data = iterator.next()).done) { result.push(data.value); } return result; } /** * Converts `map` to its key-value pairs. * * @private * @param {Object} map The map to convert. * @returns {Array} Returns the key-value pairs. */ function mapToArray(map) { var index = -1, result = Array(map.size); map.forEach(function(value, key) { result[++index] = [key, value]; }); return result; } /** * Creates a unary function that invokes `func` with its argument transformed. * * @private * @param {Function} func The function to wrap. * @param {Function} transform The argument transform. * @returns {Function} Returns the new function. */ function overArg(func, transform) { return function(arg) { return func(transform(arg)); }; } /** * Replaces all `placeholder` elements in `array` with an internal placeholder * and returns an array of their indexes. * * @private * @param {Array} array The array to modify. * @param {*} placeholder The placeholder to replace. * @returns {Array} Returns the new array of placeholder indexes. */ function replaceHolders(array, placeholder) { var index = -1, length = array.length, resIndex = 0, result = []; while (++index < length) { var value = array[index]; if (value === placeholder || value === PLACEHOLDER) { array[index] = PLACEHOLDER; result[resIndex++] = index; } } return result; } /** * Converts `set` to an array of its values. * * @private * @param {Object} set The set to convert. * @returns {Array} Returns the values. */ function setToArray(set) { var index = -1, result = Array(set.size); set.forEach(function(value) { result[++index] = value; }); return result; } /** * Converts `set` to its value-value pairs. * * @private * @param {Object} set The set to convert. * @returns {Array} Returns the value-value pairs. */ function setToPairs(set) { var index = -1, result = Array(set.size); set.forEach(function(value) { result[++index] = [value, value]; }); return result; } /** * A specialized version of `_.indexOf` which performs strict equality * comparisons of values, i.e. `===`. * * @private * @param {Array} array The array to inspect. * @param {*} value The value to search for. * @param {number} fromIndex The index to search from. * @returns {number} Returns the index of the matched value, else `-1`. */ function strictIndexOf(array, value, fromIndex) { var index = fromIndex - 1, length = array.length; while (++index < length) { if (array[index] === value) { return index; } } return -1; } /** * A specialized version of `_.lastIndexOf` which performs strict equality * comparisons of values, i.e. `===`. * * @private * @param {Array} array The array to inspect. * @param {*} value The value to search for. * @param {number} fromIndex The index to search from. * @returns {number} Returns the index of the matched value, else `-1`. */ function strictLastIndexOf(array, value, fromIndex) { var index = fromIndex + 1; while (index--) { if (array[index] === value) { return index; } } return index; } /** * Gets the number of symbols in `string`. * * @private * @param {string} string The string to inspect. * @returns {number} Returns the string size. */ function stringSize(string) { return hasUnicode(string) ? unicodeSize(string) : asciiSize(string); } /** * Converts `string` to an array. * * @private * @param {string} string The string to convert. * @returns {Array} Returns the converted array. */ function stringToArray(string) { return hasUnicode(string) ? unicodeToArray(string) : asciiToArray(string); } /** * Used by `_.unescape` to convert HTML entities to characters. * * @private * @param {string} chr The matched character to unescape. * @returns {string} Returns the unescaped character. */ var unescapeHtmlChar = basePropertyOf(htmlUnescapes); /** * Gets the size of a Unicode `string`. * * @private * @param {string} string The string inspect. * @returns {number} Returns the string size. */ function unicodeSize(string) { var result = reUnicode.lastIndex = 0; while (reUnicode.test(string)) { ++result; } return result; } /** * Converts a Unicode `string` to an array. * * @private * @param {string} string The string to convert. * @returns {Array} Returns the converted array. */ function unicodeToArray(string) { return string.match(reUnicode) || []; } /** * Splits a Unicode `string` into an array of its words. * * @private * @param {string} The string to inspect. * @returns {Array} Returns the words of `string`. */ function unicodeWords(string) { return string.match(reUnicodeWord) || []; } /*--------------------------------------------------------------------------*/ /** * Create a new pristine `lodash` function using the `context` object. * * @static * @memberOf _ * @since 1.1.0 * @category Util * @param {Object} [context=root] The context object. * @returns {Function} Returns a new `lodash` function. * @example * * _.mixin({ 'foo': _.constant('foo') }); * * var lodash = _.runInContext(); * lodash.mixin({ 'bar': lodash.constant('bar') }); * * _.isFunction(_.foo); * // => true * _.isFunction(_.bar); * // => false * * lodash.isFunction(lodash.foo); * // => false * lodash.isFunction(lodash.bar); * // => true * * // Create a suped-up `defer` in Node.js. * var defer = _.runInContext({ 'setTimeout': setImmediate }).defer; */ var runInContext = (function runInContext(context) { context = context == null ? root : _.defaults(root.Object(), context, _.pick(root, contextProps)); /** Built-in constructor references. */ var Array = context.Array, Date = context.Date, Error = context.Error, Function = context.Function, Math = context.Math, Object = context.Object, RegExp = context.RegExp, String = context.String, TypeError = context.TypeError; /** Used for built-in method references. */ var arrayProto = Array.prototype, funcProto = Function.prototype, objectProto = Object.prototype; /** Used to detect overreaching core-js shims. */ var coreJsData = context['__core-js_shared__']; /** Used to resolve the decompiled source of functions. */ var funcToString = funcProto.toString; /** Used to check objects for own properties. */ var hasOwnProperty = objectProto.hasOwnProperty; /** Used to generate unique IDs. */ var idCounter = 0; /** Used to detect methods masquerading as native. */ var maskSrcKey = (function() { var uid = /[^.]+$/.exec(coreJsData && coreJsData.keys && coreJsData.keys.IE_PROTO || ''); return uid ? ('Symbol(src)_1.' + uid) : ''; }()); /** * Used to resolve the * [`toStringTag`](http://ecma-international.org/ecma-262/7.0/#sec-object.prototype.tostring) * of values. */ var nativeObjectToString = objectProto.toString; /** Used to infer the `Object` constructor. */ var objectCtorString = funcToString.call(Object); /** Used to restore the original `_` reference in `_.noConflict`. */ var oldDash = root._; /** Used to detect if a method is native. */ var reIsNative = RegExp('^' + funcToString.call(hasOwnProperty).replace(reRegExpChar, '\\$&') .replace(/hasOwnProperty|(function).*?(?=\\\()| for .+?(?=\\\])/g, '$1.*?') + '$' ); /** Built-in value references. */ var Buffer = moduleExports ? context.Buffer : undefined, Symbol = context.Symbol, Uint8Array = context.Uint8Array, allocUnsafe = Buffer ? Buffer.allocUnsafe : undefined, getPrototype = overArg(Object.getPrototypeOf, Object), objectCreate = Object.create, propertyIsEnumerable = objectProto.propertyIsEnumerable, splice = arrayProto.splice, spreadableSymbol = Symbol ? Symbol.isConcatSpreadable : undefined, symIterator = Symbol ? Symbol.iterator : undefined, symToStringTag = Symbol ? Symbol.toStringTag : undefined; var defineProperty = (function() { try { var func = getNative(Object, 'defineProperty'); func({}, '', {}); return func; } catch (e) {} }()); /** Mocked built-ins. */ var ctxClearTimeout = context.clearTimeout !== root.clearTimeout && context.clearTimeout, ctxNow = Date && Date.now !== root.Date.now && Date.now, ctxSetTimeout = context.setTimeout !== root.setTimeout && context.setTimeout; /* Built-in method references for those with the same name as other `lodash` methods. */ var nativeCeil = Math.ceil, nativeFloor = Math.floor, nativeGetSymbols = Object.getOwnPropertySymbols, nativeIsBuffer = Buffer ? Buffer.isBuffer : undefined, nativeIsFinite = context.isFinite, nativeJoin = arrayProto.join, nativeKeys = overArg(Object.keys, Object), nativeMax = Math.max, nativeMin = Math.min, nativeNow = Date.now, nativeParseInt = context.parseInt, nativeRandom = Math.random, nativeReverse = arrayProto.reverse; /* Built-in method references that are verified to be native. */ var DataView = getNative(context, 'DataView'), Map = getNative(context, 'Map'), Promise = getNative(context, 'Promise'), Set = getNative(context, 'Set'), WeakMap = getNative(context, 'WeakMap'), nativeCreate = getNative(Object, 'create'); /** Used to store function metadata. */ var metaMap = WeakMap && new WeakMap; /** Used to lookup unminified function names. */ var realNames = {}; /** Used to detect maps, sets, and weakmaps. */ var dataViewCtorString = toSource(DataView), mapCtorString = toSource(Map), promiseCtorString = toSource(Promise), setCtorString = toSource(Set), weakMapCtorString = toSource(WeakMap); /** Used to convert symbols to primitives and strings. */ var symbolProto = Symbol ? Symbol.prototype : undefined, symbolValueOf = symbolProto ? symbolProto.valueOf : undefined, symbolToString = symbolProto ? symbolProto.toString : undefined; /*------------------------------------------------------------------------*/ /** * Creates a `lodash` object which wraps `value` to enable implicit method * chain sequences. Methods that operate on and return arrays, collections, * and functions can be chained together. Methods that retrieve a single value * or may return a primitive value will automatically end the chain sequence * and return the unwrapped value. Otherwise, the value must be unwrapped * with `_#value`. * * Explicit chain sequences, which must be unwrapped with `_#value`, may be * enabled using `_.chain`. * * The execution of chained methods is lazy, that is, it's deferred until * `_#value` is implicitly or explicitly called. * * Lazy evaluation allows several methods to support shortcut fusion. * Shortcut fusion is an optimization to merge iteratee calls; this avoids * the creation of intermediate arrays and can greatly reduce the number of * iteratee executions. Sections of a chain sequence qualify for shortcut * fusion if the section is applied to an array and iteratees accept only * one argument. The heuristic for whether a section qualifies for shortcut * fusion is subject to change. * * Chaining is supported in custom builds as long as the `_#value` method is * directly or indirectly included in the build. * * In addition to lodash methods, wrappers have `Array` and `String` methods. * * The wrapper `Array` methods are: * `concat`, `join`, `pop`, `push`, `shift`, `sort`, `splice`, and `unshift` * * The wrapper `String` methods are: * `replace` and `split` * * The wrapper methods that support shortcut fusion are: * `at`, `compact`, `drop`, `dropRight`, `dropWhile`, `filter`, `find`, * `findLast`, `head`, `initial`, `last`, `map`, `reject`, `reverse`, `slice`, * `tail`, `take`, `takeRight`, `takeRightWhile`, `takeWhile`, and `toArray` * * The chainable wrapper methods are: * `after`, `ary`, `assign`, `assignIn`, `assignInWith`, `assignWith`, `at`, * `before`, `bind`, `bindAll`, `bindKey`, `castArray`, `chain`, `chunk`, * `commit`, `compact`, `concat`, `conforms`, `constant`, `countBy`, `create`, * `curry`, `debounce`, `defaults`, `defaultsDeep`, `defer`, `delay`, * `difference`, `differenceBy`, `differenceWith`, `drop`, `dropRight`, * `dropRightWhile`, `dropWhile`, `extend`, `extendWith`, `fill`, `filter`, * `flatMap`, `flatMapDeep`, `flatMapDepth`, `flatten`, `flattenDeep`, * `flattenDepth`, `flip`, `flow`, `flowRight`, `fromPairs`, `functions`, * `functionsIn`, `groupBy`, `initial`, `intersection`, `intersectionBy`, * `intersectionWith`, `invert`, `invertBy`, `invokeMap`, `iteratee`, `keyBy`, * `keys`, `keysIn`, `map`, `mapKeys`, `mapValues`, `matches`, `matchesProperty`, * `memoize`, `merge`, `mergeWith`, `method`, `methodOf`, `mixin`, `negate`, * `nthArg`, `omit`, `omitBy`, `once`, `orderBy`, `over`, `overArgs`, * `overEvery`, `overSome`, `partial`, `partialRight`, `partition`, `pick`, * `pickBy`, `plant`, `property`, `propertyOf`, `pull`, `pullAll`, `pullAllBy`, * `pullAllWith`, `pullAt`, `push`, `range`, `rangeRight`, `rearg`, `reject`, * `remove`, `rest`, `reverse`, `sampleSize`, `set`, `setWith`, `shuffle`, * `slice`, `sort`, `sortBy`, `splice`, `spread`, `tail`, `take`, `takeRight`, * `takeRightWhile`, `takeWhile`, `tap`, `throttle`, `thru`, `toArray`, * `toPairs`, `toPairsIn`, `toPath`, `toPlainObject`, `transform`, `unary`, * `union`, `unionBy`, `unionWith`, `uniq`, `uniqBy`, `uniqWith`, `unset`, * `unshift`, `unzip`, `unzipWith`, `update`, `updateWith`, `values`, * `valuesIn`, `without`, `wrap`, `xor`, `xorBy`, `xorWith`, `zip`, * `zipObject`, `zipObjectDeep`, and `zipWith` * * The wrapper methods that are **not** chainable by default are: * `add`, `attempt`, `camelCase`, `capitalize`, `ceil`, `clamp`, `clone`, * `cloneDeep`, `cloneDeepWith`, `cloneWith`, `conformsTo`, `deburr`, * `defaultTo`, `divide`, `each`, `eachRight`, `endsWith`, `eq`, `escape`, * `escapeRegExp`, `every`, `find`, `findIndex`, `findKey`, `findLast`, * `findLastIndex`, `findLastKey`, `first`, `floor`, `forEach`, `forEachRight`, * `forIn`, `forInRight`, `forOwn`, `forOwnRight`, `get`, `gt`, `gte`, `has`, * `hasIn`, `head`, `identity`, `includes`, `indexOf`, `inRange`, `invoke`, * `isArguments`, `isArray`, `isArrayBuffer`, `isArrayLike`, `isArrayLikeObject`, * `isBoolean`, `isBuffer`, `isDate`, `isElement`, `isEmpty`, `isEqual`, * `isEqualWith`, `isError`, `isFinite`, `isFunction`, `isInteger`, `isLength`, * `isMap`, `isMatch`, `isMatchWith`, `isNaN`, `isNative`, `isNil`, `isNull`, * `isNumber`, `isObject`, `isObjectLike`, `isPlainObject`, `isRegExp`, * `isSafeInteger`, `isSet`, `isString`, `isUndefined`, `isTypedArray`, * `isWeakMap`, `isWeakSet`, `join`, `kebabCase`, `last`, `lastIndexOf`, * `lowerCase`, `lowerFirst`, `lt`, `lte`, `max`, `maxBy`, `mean`, `meanBy`, * `min`, `minBy`, `multiply`, `noConflict`, `noop`, `now`, `nth`, `pad`, * `padEnd`, `padStart`, `parseInt`, `pop`, `random`, `reduce`, `reduceRight`, * `repeat`, `result`, `round`, `runInContext`, `sample`, `shift`, `size`, * `snakeCase`, `some`, `sortedIndex`, `sortedIndexBy`, `sortedLastIndex`, * `sortedLastIndexBy`, `startCase`, `startsWith`, `stubArray`, `stubFalse`, * `stubObject`, `stubString`, `stubTrue`, `subtract`, `sum`, `sumBy`, * `template`, `times`, `toFinite`, `toInteger`, `toJSON`, `toLength`, * `toLower`, `toNumber`, `toSafeInteger`, `toString`, `toUpper`, `trim`, * `trimEnd`, `trimStart`, `truncate`, `unescape`, `uniqueId`, `upperCase`, * `upperFirst`, `value`, and `words` * * @name _ * @constructor * @category Seq * @param {*} value The value to wrap in a `lodash` instance. * @returns {Object} Returns the new `lodash` wrapper instance. * @example * * function square(n) { * return n * n; * } * * var wrapped = _([1, 2, 3]); * * // Returns an unwrapped value. * wrapped.reduce(_.add); * // => 6 * * // Returns a wrapped value. * var squares = wrapped.map(square); * * _.isArray(squares); * // => false * * _.isArray(squares.value()); * // => true */ function lodash(value) { if (isObjectLike(value) && !isArray(value) && !(value instanceof LazyWrapper)) { if (value instanceof LodashWrapper) { return value; } if (hasOwnProperty.call(value, '__wrapped__')) { return wrapperClone(value); } } return new LodashWrapper(value); } /** * The base implementation of `_.create` without support for assigning * properties to the created object. * * @private * @param {Object} proto The object to inherit from. * @returns {Object} Returns the new object. */ var baseCreate = (function() { function object() {} return function(proto) { if (!isObject(proto)) { return {}; } if (objectCreate) { return objectCreate(proto); } object.prototype = proto; var result = new object; object.prototype = undefined; return result; }; }()); /** * The function whose prototype chain sequence wrappers inherit from. * * @private */ function baseLodash() { // No operation performed. } /** * The base constructor for creating `lodash` wrapper objects. * * @private * @param {*} value The value to wrap. * @param {boolean} [chainAll] Enable explicit method chain sequences. */ function LodashWrapper(value, chainAll) { this.__wrapped__ = value; this.__actions__ = []; this.__chain__ = !!chainAll; this.__index__ = 0; this.__values__ = undefined; } /** * By default, the template delimiters used by lodash are like those in * embedded Ruby (ERB) as well as ES2015 template strings. Change the * following template settings to use alternative delimiters. * * @static * @memberOf _ * @type {Object} */ lodash.templateSettings = { /** * Used to detect `data` property values to be HTML-escaped. * * @memberOf _.templateSettings * @type {RegExp} */ 'escape': reEscape, /** * Used to detect code to be evaluated. * * @memberOf _.templateSettings * @type {RegExp} */ 'evaluate': reEvaluate, /** * Used to detect `data` property values to inject. * * @memberOf _.templateSettings * @type {RegExp} */ 'interpolate': reInterpolate, /** * Used to reference the data object in the template text. * * @memberOf _.templateSettings * @type {string} */ 'variable': '', /** * Used to import variables into the compiled template. * * @memberOf _.templateSettings * @type {Object} */ 'imports': { /** * A reference to the `lodash` function. * * @memberOf _.templateSettings.imports * @type {Function} */ '_': lodash } }; // Ensure wrappers are instances of `baseLodash`. lodash.prototype = baseLodash.prototype; lodash.prototype.constructor = lodash; LodashWrapper.prototype = baseCreate(baseLodash.prototype); LodashWrapper.prototype.constructor = LodashWrapper; /*------------------------------------------------------------------------*/ /** * Creates a lazy wrapper object which wraps `value` to enable lazy evaluation. * * @private * @constructor * @param {*} value The value to wrap. */ function LazyWrapper(value) { this.__wrapped__ = value; this.__actions__ = []; this.__dir__ = 1; this.__filtered__ = false; this.__iteratees__ = []; this.__takeCount__ = MAX_ARRAY_LENGTH; this.__views__ = []; } /** * Creates a clone of the lazy wrapper object. * * @private * @name clone * @memberOf LazyWrapper * @returns {Object} Returns the cloned `LazyWrapper` object. */ function lazyClone() { var result = new LazyWrapper(this.__wrapped__); result.__actions__ = copyArray(this.__actions__); result.__dir__ = this.__dir__; result.__filtered__ = this.__filtered__; result.__iteratees__ = copyArray(this.__iteratees__); result.__takeCount__ = this.__takeCount__; result.__views__ = copyArray(this.__views__); return result; } /** * Reverses the direction of lazy iteration. * * @private * @name reverse * @memberOf LazyWrapper * @returns {Object} Returns the new reversed `LazyWrapper` object. */ function lazyReverse() { if (this.__filtered__) { var result = new LazyWrapper(this); result.__dir__ = -1; result.__filtered__ = true; } else { result = this.clone(); result.__dir__ *= -1; } return result; } /** * Extracts the unwrapped value from its lazy wrapper. * * @private * @name value * @memberOf LazyWrapper * @returns {*} Returns the unwrapped value. */ function lazyValue() { var array = this.__wrapped__.value(), dir = this.__dir__, isArr = isArray(array), isRight = dir < 0, arrLength = isArr ? array.length : 0, view = getView(0, arrLength, this.__views__), start = view.start, end = view.end, length = end - start, index = isRight ? end : (start - 1), iteratees = this.__iteratees__, iterLength = iteratees.length, resIndex = 0, takeCount = nativeMin(length, this.__takeCount__); if (!isArr || (!isRight && arrLength == length && takeCount == length)) { return baseWrapperValue(array, this.__actions__); } var result = []; outer: while (length-- && resIndex < takeCount) { index += dir; var iterIndex = -1, value = array[index]; while (++iterIndex < iterLength) { var data = iteratees[iterIndex], iteratee = data.iteratee, type = data.type, computed = iteratee(value); if (type == LAZY_MAP_FLAG) { value = computed; } else if (!computed) { if (type == LAZY_FILTER_FLAG) { continue outer; } else { break outer; } } } result[resIndex++] = value; } return result; } // Ensure `LazyWrapper` is an instance of `baseLodash`. LazyWrapper.prototype = baseCreate(baseLodash.prototype); LazyWrapper.prototype.constructor = LazyWrapper; /*------------------------------------------------------------------------*/ /** * Creates a hash object. * * @private * @constructor * @param {Array} [entries] The key-value pairs to cache. */ function Hash(entries) { var index = -1, length = entries == null ? 0 : entries.length; this.clear(); while (++index < length) { var entry = entries[index]; this.set(entry[0], entry[1]); } } /** * Removes all key-value entries from the hash. * * @private * @name clear * @memberOf Hash */ function hashClear() { this.__data__ = nativeCreate ? nativeCreate(null) : {}; this.size = 0; } /** * Removes `key` and its value from the hash. * * @private * @name delete * @memberOf Hash * @param {Object} hash The hash to modify. * @param {string} key The key of the value to remove. * @returns {boolean} Returns `true` if the entry was removed, else `false`. */ function hashDelete(key) { var result = this.has(key) && delete this.__data__[key]; this.size -= result ? 1 : 0; return result; } /** * Gets the hash value for `key`. * * @private * @name get * @memberOf Hash * @param {string} key The key of the value to get. * @returns {*} Returns the entry value. */ function hashGet(key) { var data = this.__data__; if (nativeCreate) { var result = data[key]; return result === HASH_UNDEFINED ? undefined : result; } return hasOwnProperty.call(data, key) ? data[key] : undefined; } /** * Checks if a hash value for `key` exists. * * @private * @name has * @memberOf Hash * @param {string} key The key of the entry to check. * @returns {boolean} Returns `true` if an entry for `key` exists, else `false`. */ function hashHas(key) { var data = this.__data__; return nativeCreate ? (data[key] !== undefined) : hasOwnProperty.call(data, key); } /** * Sets the hash `key` to `value`. * * @private * @name set * @memberOf Hash * @param {string} key The key of the value to set. * @param {*} value The value to set. * @returns {Object} Returns the hash instance. */ function hashSet(key, value) { var data = this.__data__; this.size += this.has(key) ? 0 : 1; data[key] = (nativeCreate && value === undefined) ? HASH_UNDEFINED : value; return this; } // Add methods to `Hash`. Hash.prototype.clear = hashClear; Hash.prototype['delete'] = hashDelete; Hash.prototype.get = hashGet; Hash.prototype.has = hashHas; Hash.prototype.set = hashSet; /*------------------------------------------------------------------------*/ /** * Creates an list cache object. * * @private * @constructor * @param {Array} [entries] The key-value pairs to cache. */ function ListCache(entries) { var index = -1, length = entries == null ? 0 : entries.length; this.clear(); while (++index < length) { var entry = entries[index]; this.set(entry[0], entry[1]); } } /** * Removes all key-value entries from the list cache. * * @private * @name clear * @memberOf ListCache */ function listCacheClear() { this.__data__ = []; this.size = 0; } /** * Removes `key` and its value from the list cache. * * @private * @name delete * @memberOf ListCache * @param {string} key The key of the value to remove. * @returns {boolean} Returns `true` if the entry was removed, else `false`. */ function listCacheDelete(key) { var data = this.__data__, index = assocIndexOf(data, key); if (index < 0) { return false; } var lastIndex = data.length - 1; if (index == lastIndex) { data.pop(); } else { splice.call(data, index, 1); } --this.size; return true; } /** * Gets the list cache value for `key`. * * @private * @name get * @memberOf ListCache * @param {string} key The key of the value to get. * @returns {*} Returns the entry value. */ function listCacheGet(key) { var data = this.__data__, index = assocIndexOf(data, key); return index < 0 ? undefined : data[index][1]; } /** * Checks if a list cache value for `key` exists. * * @private * @name has * @memberOf ListCache * @param {string} key The key of the entry to check. * @returns {boolean} Returns `true` if an entry for `key` exists, else `false`. */ function listCacheHas(key) { return assocIndexOf(this.__data__, key) > -1; } /** * Sets the list cache `key` to `value`. * * @private * @name set * @memberOf ListCache * @param {string} key The key of the value to set. * @param {*} value The value to set. * @returns {Object} Returns the list cache instance. */ function listCacheSet(key, value) { var data = this.__data__, index = assocIndexOf(data, key); if (index < 0) { ++this.size; data.push([key, value]); } else { data[index][1] = value; } return this; } // Add methods to `ListCache`. ListCache.prototype.clear = listCacheClear; ListCache.prototype['delete'] = listCacheDelete; ListCache.prototype.get = listCacheGet; ListCache.prototype.has = listCacheHas; ListCache.prototype.set = listCacheSet; /*------------------------------------------------------------------------*/ /** * Creates a map cache object to store key-value pairs. * * @private * @constructor * @param {Array} [entries] The key-value pairs to cache. */ function MapCache(entries) { var index = -1, length = entries == null ? 0 : entries.length; this.clear(); while (++index < length) { var entry = entries[index]; this.set(entry[0], entry[1]); } } /** * Removes all key-value entries from the map. * * @private * @name clear * @memberOf MapCache */ function mapCacheClear() { this.size = 0; this.__data__ = { 'hash': new Hash, 'map': new (Map || ListCache), 'string': new Hash }; } /** * Removes `key` and its value from the map. * * @private * @name delete * @memberOf MapCache * @param {string} key The key of the value to remove. * @returns {boolean} Returns `true` if the entry was removed, else `false`. */ function mapCacheDelete(key) { var result = getMapData(this, key)['delete'](key); this.size -= result ? 1 : 0; return result; } /** * Gets the map value for `key`. * * @private * @name get * @memberOf MapCache * @param {string} key The key of the value to get. * @returns {*} Returns the entry value. */ function mapCacheGet(key) { return getMapData(this, key).get(key); } /** * Checks if a map value for `key` exists. * * @private * @name has * @memberOf MapCache * @param {string} key The key of the entry to check. * @returns {boolean} Returns `true` if an entry for `key` exists, else `false`. */ function mapCacheHas(key) { return getMapData(this, key).has(key); } /** * Sets the map `key` to `value`. * * @private * @name set * @memberOf MapCache * @param {string} key The key of the value to set. * @param {*} value The value to set. * @returns {Object} Returns the map cache instance. */ function mapCacheSet(key, value) { var data = getMapData(this, key), size = data.size; data.set(key, value); this.size += data.size == size ? 0 : 1; return this; } // Add methods to `MapCache`. MapCache.prototype.clear = mapCacheClear; MapCache.prototype['delete'] = mapCacheDelete; MapCache.prototype.get = mapCacheGet; MapCache.prototype.has = mapCacheHas; MapCache.prototype.set = mapCacheSet; /*------------------------------------------------------------------------*/ /** * * Creates an array cache object to store unique values. * * @private * @constructor * @param {Array} [values] The values to cache. */ function SetCache(values) { var index = -1, length = values == null ? 0 : values.length; this.__data__ = new MapCache; while (++index < length) { this.add(values[index]); } } /** * Adds `value` to the array cache. * * @private * @name add * @memberOf SetCache * @alias push * @param {*} value The value to cache. * @returns {Object} Returns the cache instance. */ function setCacheAdd(value) { this.__data__.set(value, HASH_UNDEFINED); return this; } /** * Checks if `value` is in the array cache. * * @private * @name has * @memberOf SetCache * @param {*} value The value to search for. * @returns {number} Returns `true` if `value` is found, else `false`. */ function setCacheHas(value) { return this.__data__.has(value); } // Add methods to `SetCache`. SetCache.prototype.add = SetCache.prototype.push = setCacheAdd; SetCache.prototype.has = setCacheHas; /*------------------------------------------------------------------------*/ /** * Creates a stack cache object to store key-value pairs. * * @private * @constructor * @param {Array} [entries] The key-value pairs to cache. */ function Stack(entries) { var data = this.__data__ = new ListCache(entries); this.size = data.size; } /** * Removes all key-value entries from the stack. * * @private * @name clear * @memberOf Stack */ function stackClear() { this.__data__ = new ListCache; this.size = 0; } /** * Removes `key` and its value from the stack. * * @private * @name delete * @memberOf Stack * @param {string} key The key of the value to remove. * @returns {boolean} Returns `true` if the entry was removed, else `false`. */ function stackDelete(key) { var data = this.__data__, result = data['delete'](key); this.size = data.size; return result; } /** * Gets the stack value for `key`. * * @private * @name get * @memberOf Stack * @param {string} key The key of the value to get. * @returns {*} Returns the entry value. */ function stackGet(key) { return this.__data__.get(key); } /** * Checks if a stack value for `key` exists. * * @private * @name has * @memberOf Stack * @param {string} key The key of the entry to check. * @returns {boolean} Returns `true` if an entry for `key` exists, else `false`. */ function stackHas(key) { return this.__data__.has(key); } /** * Sets the stack `key` to `value`. * * @private * @name set * @memberOf Stack * @param {string} key The key of the value to set. * @param {*} value The value to set. * @returns {Object} Returns the stack cache instance. */ function stackSet(key, value) { var data = this.__data__; if (data instanceof ListCache) { var pairs = data.__data__; if (!Map || (pairs.length < LARGE_ARRAY_SIZE - 1)) { pairs.push([key, value]); this.size = ++data.size; return this; } data = this.__data__ = new MapCache(pairs); } data.set(key, value); this.size = data.size; return this; } // Add methods to `Stack`. Stack.prototype.clear = stackClear; Stack.prototype['delete'] = stackDelete; Stack.prototype.get = stackGet; Stack.prototype.has = stackHas; Stack.prototype.set = stackSet; /*------------------------------------------------------------------------*/ /** * Creates an array of the enumerable property names of the array-like `value`. * * @private * @param {*} value The value to query. * @param {boolean} inherited Specify returning inherited property names. * @returns {Array} Returns the array of property names. */ function arrayLikeKeys(value, inherited) { var isArr = isArray(value), isArg = !isArr && isArguments(value), isBuff = !isArr && !isArg && isBuffer(value), isType = !isArr && !isArg && !isBuff && isTypedArray(value), skipIndexes = isArr || isArg || isBuff || isType, result = skipIndexes ? baseTimes(value.length, String) : [], length = result.length; for (var key in value) { if ((inherited || hasOwnProperty.call(value, key)) && !(skipIndexes && ( // Safari 9 has enumerable `arguments.length` in strict mode. key == 'length' || // Node.js 0.10 has enumerable non-index properties on buffers. (isBuff && (key == 'offset' || key == 'parent')) || // PhantomJS 2 has enumerable non-index properties on typed arrays. (isType && (key == 'buffer' || key == 'byteLength' || key == 'byteOffset')) || // Skip index properties. isIndex(key, length) ))) { result.push(key); } } return result; } /** * A specialized version of `_.sample` for arrays. * * @private * @param {Array} array The array to sample. * @returns {*} Returns the random element. */ function arraySample(array) { var length = array.length; return length ? array[baseRandom(0, length - 1)] : undefined; } /** * A specialized version of `_.sampleSize` for arrays. * * @private * @param {Array} array The array to sample. * @param {number} n The number of elements to sample. * @returns {Array} Returns the random elements. */ function arraySampleSize(array, n) { return shuffleSelf(copyArray(array), baseClamp(n, 0, array.length)); } /** * A specialized version of `_.shuffle` for arrays. * * @private * @param {Array} array The array to shuffle. * @returns {Array} Returns the new shuffled array. */ function arrayShuffle(array) { return shuffleSelf(copyArray(array)); } /** * This function is like `assignValue` except that it doesn't assign * `undefined` values. * * @private * @param {Object} object The object to modify. * @param {string} key The key of the property to assign. * @param {*} value The value to assign. */ function assignMergeValue(object, key, value) { if ((value !== undefined && !eq(object[key], value)) || (value === undefined && !(key in object))) { baseAssignValue(object, key, value); } } /** * Assigns `value` to `key` of `object` if the existing value is not equivalent * using [`SameValueZero`](http://ecma-international.org/ecma-262/7.0/#sec-samevaluezero) * for equality comparisons. * * @private * @param {Object} object The object to modify. * @param {string} key The key of the property to assign. * @param {*} value The value to assign. */ function assignValue(object, key, value) { var objValue = object[key]; if (!(hasOwnProperty.call(object, key) && eq(objValue, value)) || (value === undefined && !(key in object))) { baseAssignValue(object, key, value); } } /** * Gets the index at which the `key` is found in `array` of key-value pairs. * * @private * @param {Array} array The array to inspect. * @param {*} key The key to search for. * @returns {number} Returns the index of the matched value, else `-1`. */ function assocIndexOf(array, key) { var length = array.length; while (length--) { if (eq(array[length][0], key)) { return length; } } return -1; } /** * Aggregates elements of `collection` on `accumulator` with keys transformed * by `iteratee` and values set by `setter`. * * @private * @param {Array|Object} collection The collection to iterate over. * @param {Function} setter The function to set `accumulator` values. * @param {Function} iteratee The iteratee to transform keys. * @param {Object} accumulator The initial aggregated object. * @returns {Function} Returns `accumulator`. */ function baseAggregator(collection, setter, iteratee, accumulator) { baseEach(collection, function(value, key, collection) { setter(accumulator, value, iteratee(value), collection); }); return accumulator; } /** * The base implementation of `_.assign` without support for multiple sources * or `customizer` functions. * * @private * @param {Object} object The destination object. * @param {Object} source The source object. * @returns {Object} Returns `object`. */ function baseAssign(object, source) { return object && copyObject(source, keys(source), object); } /** * The base implementation of `_.assignIn` without support for multiple sources * or `customizer` functions. * * @private * @param {Object} object The destination object. * @param {Object} source The source object. * @returns {Object} Returns `object`. */ function baseAssignIn(object, source) { return object && copyObject(source, keysIn(source), object); } /** * The base implementation of `assignValue` and `assignMergeValue` without * value checks. * * @private * @param {Object} object The object to modify. * @param {string} key The key of the property to assign. * @param {*} value The value to assign. */ function baseAssignValue(object, key, value) { if (key == '__proto__' && defineProperty) { defineProperty(object, key, { 'configurable': true, 'enumerable': true, 'value': value, 'writable': true }); } else { object[key] = value; } } /** * The base implementation of `_.at` without support for individual paths. * * @private * @param {Object} object The object to iterate over. * @param {string[]} paths The property paths to pick. * @returns {Array} Returns the picked elements. */ function baseAt(object, paths) { var index = -1, length = paths.length, result = Array(length), skip = object == null; while (++index < length) { result[index] = skip ? undefined : get(object, paths[index]); } return result; } /** * The base implementation of `_.clamp` which doesn't coerce arguments. * * @private * @param {number} number The number to clamp. * @param {number} [lower] The lower bound. * @param {number} upper The upper bound. * @returns {number} Returns the clamped number. */ function baseClamp(number, lower, upper) { if (number === number) { if (upper !== undefined) { number = number <= upper ? number : upper; } if (lower !== undefined) { number = number >= lower ? number : lower; } } return number; } /** * The base implementation of `_.clone` and `_.cloneDeep` which tracks * traversed objects. * * @private * @param {*} value The value to clone. * @param {boolean} bitmask The bitmask flags. * 1 - Deep clone * 2 - Flatten inherited properties * 4 - Clone symbols * @param {Function} [customizer] The function to customize cloning. * @param {string} [key] The key of `value`. * @param {Object} [object] The parent object of `value`. * @param {Object} [stack] Tracks traversed objects and their clone counterparts. * @returns {*} Returns the cloned value. */ function baseClone(value, bitmask, customizer, key, object, stack) { var result, isDeep = bitmask & CLONE_DEEP_FLAG, isFlat = bitmask & CLONE_FLAT_FLAG, isFull = bitmask & CLONE_SYMBOLS_FLAG; if (customizer) { result = object ? customizer(value, key, object, stack) : customizer(value); } if (result !== undefined) { return result; } if (!isObject(value)) { return value; } var isArr = isArray(value); if (isArr) { result = initCloneArray(value); if (!isDeep) { return copyArray(value, result); } } else { var tag = getTag(value), isFunc = tag == funcTag || tag == genTag; if (isBuffer(value)) { return cloneBuffer(value, isDeep); } if (tag == objectTag || tag == argsTag || (isFunc && !object)) { result = (isFlat || isFunc) ? {} : initCloneObject(value); if (!isDeep) { return isFlat ? copySymbolsIn(value, baseAssignIn(result, value)) : copySymbols(value, baseAssign(result, value)); } } else { if (!cloneableTags[tag]) { return object ? value : {}; } result = initCloneByTag(value, tag, isDeep); } } // Check for circular references and return its corresponding clone. stack || (stack = new Stack); var stacked = stack.get(value); if (stacked) { return stacked; } stack.set(value, result); if (isSet(value)) { value.forEach(function(subValue) { result.add(baseClone(subValue, bitmask, customizer, subValue, value, stack)); }); } else if (isMap(value)) { value.forEach(function(subValue, key) { result.set(key, baseClone(subValue, bitmask, customizer, key, value, stack)); }); } var keysFunc = isFull ? (isFlat ? getAllKeysIn : getAllKeys) : (isFlat ? keysIn : keys); var props = isArr ? undefined : keysFunc(value); arrayEach(props || value, function(subValue, key) { if (props) { key = subValue; subValue = value[key]; } // Recursively populate clone (susceptible to call stack limits). assignValue(result, key, baseClone(subValue, bitmask, customizer, key, value, stack)); }); return result; } /** * The base implementation of `_.conforms` which doesn't clone `source`. * * @private * @param {Object} source The object of property predicates to conform to. * @returns {Function} Returns the new spec function. */ function baseConforms(source) { var props = keys(source); return function(object) { return baseConformsTo(object, source, props); }; } /** * The base implementation of `_.conformsTo` which accepts `props` to check. * * @private * @param {Object} object The object to inspect. * @param {Object} source The object of property predicates to conform to. * @returns {boolean} Returns `true` if `object` conforms, else `false`. */ function baseConformsTo(object, source, props) { var length = props.length; if (object == null) { return !length; } object = Object(object); while (length--) { var key = props[length], predicate = source[key], value = object[key]; if ((value === undefined && !(key in object)) || !predicate(value)) { return false; } } return true; } /** * The base implementation of `_.delay` and `_.defer` which accepts `args` * to provide to `func`. * * @private * @param {Function} func The function to delay. * @param {number} wait The number of milliseconds to delay invocation. * @param {Array} args The arguments to provide to `func`. * @returns {number|Object} Returns the timer id or timeout object. */ function baseDelay(func, wait, args) { if (typeof func != 'function') { throw new TypeError(FUNC_ERROR_TEXT); } return setTimeout(function() { func.apply(undefined, args); }, wait); } /** * The base implementation of methods like `_.difference` without support * for excluding multiple arrays or iteratee shorthands. * * @private * @param {Array} array The array to inspect. * @param {Array} values The values to exclude. * @param {Function} [iteratee] The iteratee invoked per element. * @param {Function} [comparator] The comparator invoked per element. * @returns {Array} Returns the new array of filtered values. */ function baseDifference(array, values, iteratee, comparator) { var index = -1, includes = arrayIncludes, isCommon = true, length = array.length, result = [], valuesLength = values.length; if (!length) { return result; } if (iteratee) { values = arrayMap(values, baseUnary(iteratee)); } if (comparator) { includes = arrayIncludesWith; isCommon = false; } else if (values.length >= LARGE_ARRAY_SIZE) { includes = cacheHas; isCommon = false; values = new SetCache(values); } outer: while (++index < length) { var value = array[index], computed = iteratee == null ? value : iteratee(value); value = (comparator || value !== 0) ? value : 0; if (isCommon && computed === computed) { var valuesIndex = valuesLength; while (valuesIndex--) { if (values[valuesIndex] === computed) { continue outer; } } result.push(value); } else if (!includes(values, computed, comparator)) { result.push(value); } } return result; } /** * The base implementation of `_.forEach` without support for iteratee shorthands. * * @private * @param {Array|Object} collection The collection to iterate over. * @param {Function} iteratee The function invoked per iteration. * @returns {Array|Object} Returns `collection`. */ var baseEach = createBaseEach(baseForOwn); /** * The base implementation of `_.forEachRight` without support for iteratee shorthands. * * @private * @param {Array|Object} collection The collection to iterate over. * @param {Function} iteratee The function invoked per iteration. * @returns {Array|Object} Returns `collection`. */ var baseEachRight = createBaseEach(baseForOwnRight, true); /** * The base implementation of `_.every` without support for iteratee shorthands. * * @private * @param {Array|Object} collection The collection to iterate over. * @param {Function} predicate The function invoked per iteration. * @returns {boolean} Returns `true` if all elements pass the predicate check, * else `false` */ function baseEvery(collection, predicate) { var result = true; baseEach(collection, function(value, index, collection) { result = !!predicate(value, index, collection); return result; }); return result; } /** * The base implementation of methods like `_.max` and `_.min` which accepts a * `comparator` to determine the extremum value. * * @private * @param {Array} array The array to iterate over. * @param {Function} iteratee The iteratee invoked per iteration. * @param {Function} comparator The comparator used to compare values. * @returns {*} Returns the extremum value. */ function baseExtremum(array, iteratee, comparator) { var index = -1, length = array.length; while (++index < length) { var value = array[index], current = iteratee(value); if (current != null && (computed === undefined ? (current === current && !isSymbol(current)) : comparator(current, computed) )) { var computed = current, result = value; } } return result; } /** * The base implementation of `_.fill` without an iteratee call guard. * * @private * @param {Array} array The array to fill. * @param {*} value The value to fill `array` with. * @param {number} [start=0] The start position. * @param {number} [end=array.length] The end position. * @returns {Array} Returns `array`. */ function baseFill(array, value, start, end) { var length = array.length; start = toInteger(start); if (start < 0) { start = -start > length ? 0 : (length + start); } end = (end === undefined || end > length) ? length : toInteger(end); if (end < 0) { end += length; } end = start > end ? 0 : toLength(end); while (start < end) { array[start++] = value; } return array; } /** * The base implementation of `_.filter` without support for iteratee shorthands. * * @private * @param {Array|Object} collection The collection to iterate over. * @param {Function} predicate The function invoked per iteration. * @returns {Array} Returns the new filtered array. */ function baseFilter(collection, predicate) { var result = []; baseEach(collection, function(value, index, collection) { if (predicate(value, index, collection)) { result.push(value); } }); return result; } /** * The base implementation of `_.flatten` with support for restricting flattening. * * @private * @param {Array} array The array to flatten. * @param {number} depth The maximum recursion depth. * @param {boolean} [predicate=isFlattenable] The function invoked per iteration. * @param {boolean} [isStrict] Restrict to values that pass `predicate` checks. * @param {Array} [result=[]] The initial result value. * @returns {Array} Returns the new flattened array. */ function baseFlatten(array, depth, predicate, isStrict, result) { var index = -1, length = array.length; predicate || (predicate = isFlattenable); result || (result = []); while (++index < length) { var value = array[index]; if (depth > 0 && predicate(value)) { if (depth > 1) { // Recursively flatten arrays (susceptible to call stack limits). baseFlatten(value, depth - 1, predicate, isStrict, result); } else { arrayPush(result, value); } } else if (!isStrict) { result[result.length] = value; } } return result; } /** * The base implementation of `baseForOwn` which iterates over `object` * properties returned by `keysFunc` and invokes `iteratee` for each property. * Iteratee functions may exit iteration early by explicitly returning `false`. * * @private * @param {Object} object The object to iterate over. * @param {Function} iteratee The function invoked per iteration. * @param {Function} keysFunc The function to get the keys of `object`. * @returns {Object} Returns `object`. */ var baseFor = createBaseFor(); /** * This function is like `baseFor` except that it iterates over properties * in the opposite order. * * @private * @param {Object} object The object to iterate over. * @param {Function} iteratee The function invoked per iteration. * @param {Function} keysFunc The function to get the keys of `object`. * @returns {Object} Returns `object`. */ var baseForRight = createBaseFor(true); /** * The base implementation of `_.forOwn` without support for iteratee shorthands. * * @private * @param {Object} object The object to iterate over. * @param {Function} iteratee The function invoked per iteration. * @returns {Object} Returns `object`. */ function baseForOwn(object, iteratee) { return object && baseFor(object, iteratee, keys); } /** * The base implementation of `_.forOwnRight` without support for iteratee shorthands. * * @private * @param {Object} object The object to iterate over. * @param {Function} iteratee The function invoked per iteration. * @returns {Object} Returns `object`. */ function baseForOwnRight(object, iteratee) { return object && baseForRight(object, iteratee, keys); } /** * The base implementation of `_.functions` which creates an array of * `object` function property names filtered from `props`. * * @private * @param {Object} object The object to inspect. * @param {Array} props The property names to filter. * @returns {Array} Returns the function names. */ function baseFunctions(object, props) { return arrayFilter(props, function(key) { return isFunction(object[key]); }); } /** * The base implementation of `_.get` without support for default values. * * @private * @param {Object} object The object to query. * @param {Array|string} path The path of the property to get. * @returns {*} Returns the resolved value. */ function baseGet(object, path) { path = castPath(path, object); var index = 0, length = path.length; while (object != null && index < length) { object = object[toKey(path[index++])]; } return (index && index == length) ? object : undefined; } /** * The base implementation of `getAllKeys` and `getAllKeysIn` which uses * `keysFunc` and `symbolsFunc` to get the enumerable property names and * symbols of `object`. * * @private * @param {Object} object The object to query. * @param {Function} keysFunc The function to get the keys of `object`. * @param {Function} symbolsFunc The function to get the symbols of `object`. * @returns {Array} Returns the array of property names and symbols. */ function baseGetAllKeys(object, keysFunc, symbolsFunc) { var result = keysFunc(object); return isArray(object) ? result : arrayPush(result, symbolsFunc(object)); } /** * The base implementation of `getTag` without fallbacks for buggy environments. * * @private * @param {*} value The value to query. * @returns {string} Returns the `toStringTag`. */ function baseGetTag(value) { if (value == null) { return value === undefined ? undefinedTag : nullTag; } return (symToStringTag && symToStringTag in Object(value)) ? getRawTag(value) : objectToString(value); } /** * The base implementation of `_.gt` which doesn't coerce arguments. * * @private * @param {*} value The value to compare. * @param {*} other The other value to compare. * @returns {boolean} Returns `true` if `value` is greater than `other`, * else `false`. */ function baseGt(value, other) { return value > other; } /** * The base implementation of `_.has` without support for deep paths. * * @private * @param {Object} [object] The object to query. * @param {Array|string} key The key to check. * @returns {boolean} Returns `true` if `key` exists, else `false`. */ function baseHas(object, key) { return object != null && hasOwnProperty.call(object, key); } /** * The base implementation of `_.hasIn` without support for deep paths. * * @private * @param {Object} [object] The object to query. * @param {Array|string} key The key to check. * @returns {boolean} Returns `true` if `key` exists, else `false`. */ function baseHasIn(object, key) { return object != null && key in Object(object); } /** * The base implementation of `_.inRange` which doesn't coerce arguments. * * @private * @param {number} number The number to check. * @param {number} start The start of the range. * @param {number} end The end of the range. * @returns {boolean} Returns `true` if `number` is in the range, else `false`. */ function baseInRange(number, start, end) { return number >= nativeMin(start, end) && number < nativeMax(start, end); } /** * The base implementation of methods like `_.intersection`, without support * for iteratee shorthands, that accepts an array of arrays to inspect. * * @private * @param {Array} arrays The arrays to inspect. * @param {Function} [iteratee] The iteratee invoked per element. * @param {Function} [comparator] The comparator invoked per element. * @returns {Array} Returns the new array of shared values. */ function baseIntersection(arrays, iteratee, comparator) { var includes = comparator ? arrayIncludesWith : arrayIncludes, length = arrays[0].length, othLength = arrays.length, othIndex = othLength, caches = Array(othLength), maxLength = Infinity, result = []; while (othIndex--) { var array = arrays[othIndex]; if (othIndex && iteratee) { array = arrayMap(array, baseUnary(iteratee)); } maxLength = nativeMin(array.length, maxLength); caches[othIndex] = !comparator && (iteratee || (length >= 120 && array.length >= 120)) ? new SetCache(othIndex && array) : undefined; } array = arrays[0]; var index = -1, seen = caches[0]; outer: while (++index < length && result.length < maxLength) { var value = array[index], computed = iteratee ? iteratee(value) : value; value = (comparator || value !== 0) ? value : 0; if (!(seen ? cacheHas(seen, computed) : includes(result, computed, comparator) )) { othIndex = othLength; while (--othIndex) { var cache = caches[othIndex]; if (!(cache ? cacheHas(cache, computed) : includes(arrays[othIndex], computed, comparator)) ) { continue outer; } } if (seen) { seen.push(computed); } result.push(value); } } return result; } /** * The base implementation of `_.invert` and `_.invertBy` which inverts * `object` with values transformed by `iteratee` and set by `setter`. * * @private * @param {Object} object The object to iterate over. * @param {Function} setter The function to set `accumulator` values. * @param {Function} iteratee The iteratee to transform values. * @param {Object} accumulator The initial inverted object. * @returns {Function} Returns `accumulator`. */ function baseInverter(object, setter, iteratee, accumulator) { baseForOwn(object, function(value, key, object) { setter(accumulator, iteratee(value), key, object); }); return accumulator; } /** * The base implementation of `_.invoke` without support for individual * method arguments. * * @private * @param {Object} object The object to query. * @param {Array|string} path The path of the method to invoke. * @param {Array} args The arguments to invoke the method with. * @returns {*} Returns the result of the invoked method. */ function baseInvoke(object, path, args) { path = castPath(path, object); object = parent(object, path); var func = object == null ? object : object[toKey(last(path))]; return func == null ? undefined : apply(func, object, args); } /** * The base implementation of `_.isArguments`. * * @private * @param {*} value The value to check. * @returns {boolean} Returns `true` if `value` is an `arguments` object, */ function baseIsArguments(value) { return isObjectLike(value) && baseGetTag(value) == argsTag; } /** * The base implementation of `_.isArrayBuffer` without Node.js optimizations. * * @private * @param {*} value The value to check. * @returns {boolean} Returns `true` if `value` is an array buffer, else `false`. */ function baseIsArrayBuffer(value) { return isObjectLike(value) && baseGetTag(value) == arrayBufferTag; } /** * The base implementation of `_.isDate` without Node.js optimizations. * * @private * @param {*} value The value to check. * @returns {boolean} Returns `true` if `value` is a date object, else `false`. */ function baseIsDate(value) { return isObjectLike(value) && baseGetTag(value) == dateTag; } /** * The base implementation of `_.isEqual` which supports partial comparisons * and tracks traversed objects. * * @private * @param {*} value The value to compare. * @param {*} other The other value to compare. * @param {boolean} bitmask The bitmask flags. * 1 - Unordered comparison * 2 - Partial comparison * @param {Function} [customizer] The function to customize comparisons. * @param {Object} [stack] Tracks traversed `value` and `other` objects. * @returns {boolean} Returns `true` if the values are equivalent, else `false`. */ function baseIsEqual(value, other, bitmask, customizer, stack) { if (value === other) { return true; } if (value == null || other == null || (!isObjectLike(value) && !isObjectLike(other))) { return value !== value && other !== other; } return baseIsEqualDeep(value, other, bitmask, customizer, baseIsEqual, stack); } /** * A specialized version of `baseIsEqual` for arrays and objects which performs * deep comparisons and tracks traversed objects enabling objects with circular * references to be compared. * * @private * @param {Object} object The object to compare. * @param {Object} other The other object to compare. * @param {number} bitmask The bitmask flags. See `baseIsEqual` for more details. * @param {Function} customizer The function to customize comparisons. * @param {Function} equalFunc The function to determine equivalents of values. * @param {Object} [stack] Tracks traversed `object` and `other` objects. * @returns {boolean} Returns `true` if the objects are equivalent, else `false`. */ function baseIsEqualDeep(object, other, bitmask, customizer, equalFunc, stack) { var objIsArr = isArray(object), othIsArr = isArray(other), objTag = objIsArr ? arrayTag : getTag(object), othTag = othIsArr ? arrayTag : getTag(other); objTag = objTag == argsTag ? objectTag : objTag; othTag = othTag == argsTag ? objectTag : othTag; var objIsObj = objTag == objectTag, othIsObj = othTag == objectTag, isSameTag = objTag == othTag; if (isSameTag && isBuffer(object)) { if (!isBuffer(other)) { return false; } objIsArr = true; objIsObj = false; } if (isSameTag && !objIsObj) { stack || (stack = new Stack); return (objIsArr || isTypedArray(object)) ? equalArrays(object, other, bitmask, customizer, equalFunc, stack) : equalByTag(object, other, objTag, bitmask, customizer, equalFunc, stack); } if (!(bitmask & COMPARE_PARTIAL_FLAG)) { var objIsWrapped = objIsObj && hasOwnProperty.call(object, '__wrapped__'), othIsWrapped = othIsObj && hasOwnProperty.call(other, '__wrapped__'); if (objIsWrapped || othIsWrapped) { var objUnwrapped = objIsWrapped ? object.value() : object, othUnwrapped = othIsWrapped ? other.value() : other; stack || (stack = new Stack); return equalFunc(objUnwrapped, othUnwrapped, bitmask, customizer, stack); } } if (!isSameTag) { return false; } stack || (stack = new Stack); return equalObjects(object, other, bitmask, customizer, equalFunc, stack); } /** * The base implementation of `_.isMap` without Node.js optimizations. * * @private * @param {*} value The value to check. * @returns {boolean} Returns `true` if `value` is a map, else `false`. */ function baseIsMap(value) { return isObjectLike(value) && getTag(value) == mapTag; } /** * The base implementation of `_.isMatch` without support for iteratee shorthands. * * @private * @param {Object} object The object to inspect. * @param {Object} source The object of property values to match. * @param {Array} matchData The property names, values, and compare flags to match. * @param {Function} [customizer] The function to customize comparisons. * @returns {boolean} Returns `true` if `object` is a match, else `false`. */ function baseIsMatch(object, source, matchData, customizer) { var index = matchData.length, length = index, noCustomizer = !customizer; if (object == null) { return !length; } object = Object(object); while (index--) { var data = matchData[index]; if ((noCustomizer && data[2]) ? data[1] !== object[data[0]] : !(data[0] in object) ) { return false; } } while (++index < length) { data = matchData[index]; var key = data[0], objValue = object[key], srcValue = data[1]; if (noCustomizer && data[2]) { if (objValue === undefined && !(key in object)) { return false; } } else { var stack = new Stack; if (customizer) { var result = customizer(objValue, srcValue, key, object, source, stack); } if (!(result === undefined ? baseIsEqual(srcValue, objValue, COMPARE_PARTIAL_FLAG | COMPARE_UNORDERED_FLAG, customizer, stack) : result )) { return false; } } } return true; } /** * The base implementation of `_.isNative` without bad shim checks. * * @private * @param {*} value The value to check. * @returns {boolean} Returns `true` if `value` is a native function, * else `false`. */ function baseIsNative(value) { if (!isObject(value) || isMasked(value)) { return false; } var pattern = isFunction(value) ? reIsNative : reIsHostCtor; return pattern.test(toSource(value)); } /** * The base implementation of `_.isRegExp` without Node.js optimizations. * * @private * @param {*} value The value to check. * @returns {boolean} Returns `true` if `value` is a regexp, else `false`. */ function baseIsRegExp(value) { return isObjectLike(value) && baseGetTag(value) == regexpTag; } /** * The base implementation of `_.isSet` without Node.js optimizations. * * @private * @param {*} value The value to check. * @returns {boolean} Returns `true` if `value` is a set, else `false`. */ function baseIsSet(value) { return isObjectLike(value) && getTag(value) == setTag; } /** * The base implementation of `_.isTypedArray` without Node.js optimizations. * * @private * @param {*} value The value to check. * @returns {boolean} Returns `true` if `value` is a typed array, else `false`. */ function baseIsTypedArray(value) { return isObjectLike(value) && isLength(value.length) && !!typedArrayTags[baseGetTag(value)]; } /** * The base implementation of `_.iteratee`. * * @private * @param {*} [value=_.identity] The value to convert to an iteratee. * @returns {Function} Returns the iteratee. */ function baseIteratee(value) { // Don't store the `typeof` result in a variable to avoid a JIT bug in Safari 9. // See https://bugs.webkit.org/show_bug.cgi?id=156034 for more details. if (typeof value == 'function') { return value; } if (value == null) { return identity; } if (typeof value == 'object') { return isArray(value) ? baseMatchesProperty(value[0], value[1]) : baseMatches(value); } return property(value); } /** * The base implementation of `_.keys` which doesn't treat sparse arrays as dense. * * @private * @param {Object} object The object to query. * @returns {Array} Returns the array of property names. */ function baseKeys(object) { if (!isPrototype(object)) { return nativeKeys(object); } var result = []; for (var key in Object(object)) { if (hasOwnProperty.call(object, key) && key != 'constructor') { result.push(key); } } return result; } /** * The base implementation of `_.keysIn` which doesn't treat sparse arrays as dense. * * @private * @param {Object} object The object to query. * @returns {Array} Returns the array of property names. */ function baseKeysIn(object) { if (!isObject(object)) { return nativeKeysIn(object); } var isProto = isPrototype(object), result = []; for (var key in object) { if (!(key == 'constructor' && (isProto || !hasOwnProperty.call(object, key)))) { result.push(key); } } return result; } /** * The base implementation of `_.lt` which doesn't coerce arguments. * * @private * @param {*} value The value to compare. * @param {*} other The other value to compare. * @returns {boolean} Returns `true` if `value` is less than `other`, * else `false`. */ function baseLt(value, other) { return value < other; } /** * The base implementation of `_.map` without support for iteratee shorthands. * * @private * @param {Array|Object} collection The collection to iterate over. * @param {Function} iteratee The function invoked per iteration. * @returns {Array} Returns the new mapped array. */ function baseMap(collection, iteratee) { var index = -1, result = isArrayLike(collection) ? Array(collection.length) : []; baseEach(collection, function(value, key, collection) { result[++index] = iteratee(value, key, collection); }); return result; } /** * The base implementation of `_.matches` which doesn't clone `source`. * * @private * @param {Object} source The object of property values to match. * @returns {Function} Returns the new spec function. */ function baseMatches(source) { var matchData = getMatchData(source); if (matchData.length == 1 && matchData[0][2]) { return matchesStrictComparable(matchData[0][0], matchData[0][1]); } return function(object) { return object === source || baseIsMatch(object, source, matchData); }; } /** * The base implementation of `_.matchesProperty` which doesn't clone `srcValue`. * * @private * @param {string} path The path of the property to get. * @param {*} srcValue The value to match. * @returns {Function} Returns the new spec function. */ function baseMatchesProperty(path, srcValue) { if (isKey(path) && isStrictComparable(srcValue)) { return matchesStrictComparable(toKey(path), srcValue); } return function(object) { var objValue = get(object, path); return (objValue === undefined && objValue === srcValue) ? hasIn(object, path) : baseIsEqual(srcValue, objValue, COMPARE_PARTIAL_FLAG | COMPARE_UNORDERED_FLAG); }; } /** * The base implementation of `_.merge` without support for multiple sources. * * @private * @param {Object} object The destination object. * @param {Object} source The source object. * @param {number} srcIndex The index of `source`. * @param {Function} [customizer] The function to customize merged values. * @param {Object} [stack] Tracks traversed source values and their merged * counterparts. */ function baseMerge(object, source, srcIndex, customizer, stack) { if (object === source) { return; } baseFor(source, function(srcValue, key) { stack || (stack = new Stack); if (isObject(srcValue)) { baseMergeDeep(object, source, key, srcIndex, baseMerge, customizer, stack); } else { var newValue = customizer ? customizer(safeGet(object, key), srcValue, (key + ''), object, source, stack) : undefined; if (newValue === undefined) { newValue = srcValue; } assignMergeValue(object, key, newValue); } }, keysIn); } /** * A specialized version of `baseMerge` for arrays and objects which performs * deep merges and tracks traversed objects enabling objects with circular * references to be merged. * * @private * @param {Object} object The destination object. * @param {Object} source The source object. * @param {string} key The key of the value to merge. * @param {number} srcIndex The index of `source`. * @param {Function} mergeFunc The function to merge values. * @param {Function} [customizer] The function to customize assigned values. * @param {Object} [stack] Tracks traversed source values and their merged * counterparts. */ function baseMergeDeep(object, source, key, srcIndex, mergeFunc, customizer, stack) { var objValue = safeGet(object, key), srcValue = safeGet(source, key), stacked = stack.get(srcValue); if (stacked) { assignMergeValue(object, key, stacked); return; } var newValue = customizer ? customizer(objValue, srcValue, (key + ''), object, source, stack) : undefined; var isCommon = newValue === undefined; if (isCommon) { var isArr = isArray(srcValue), isBuff = !isArr && isBuffer(srcValue), isTyped = !isArr && !isBuff && isTypedArray(srcValue); newValue = srcValue; if (isArr || isBuff || isTyped) { if (isArray(objValue)) { newValue = objValue; } else if (isArrayLikeObject(objValue)) { newValue = copyArray(objValue); } else if (isBuff) { isCommon = false; newValue = cloneBuffer(srcValue, true); } else if (isTyped) { isCommon = false; newValue = cloneTypedArray(srcValue, true); } else { newValue = []; } } else if (isPlainObject(srcValue) || isArguments(srcValue)) { newValue = objValue; if (isArguments(objValue)) { newValue = toPlainObject(objValue); } else if (!isObject(objValue) || isFunction(objValue)) { newValue = initCloneObject(srcValue); } } else { isCommon = false; } } if (isCommon) { // Recursively merge objects and arrays (susceptible to call stack limits). stack.set(srcValue, newValue); mergeFunc(newValue, srcValue, srcIndex, customizer, stack); stack['delete'](srcValue); } assignMergeValue(object, key, newValue); } /** * The base implementation of `_.nth` which doesn't coerce arguments. * * @private * @param {Array} array The array to query. * @param {number} n The index of the element to return. * @returns {*} Returns the nth element of `array`. */ function baseNth(array, n) { var length = array.length; if (!length) { return; } n += n < 0 ? length : 0; return isIndex(n, length) ? array[n] : undefined; } /** * The base implementation of `_.orderBy` without param guards. * * @private * @param {Array|Object} collection The collection to iterate over. * @param {Function[]|Object[]|string[]} iteratees The iteratees to sort by. * @param {string[]} orders The sort orders of `iteratees`. * @returns {Array} Returns the new sorted array. */ function baseOrderBy(collection, iteratees, orders) { if (iteratees.length) { iteratees = arrayMap(iteratees, function(iteratee) { if (isArray(iteratee)) { return function(value) { return baseGet(value, iteratee.length === 1 ? iteratee[0] : iteratee); } } return iteratee; }); } else { iteratees = [identity]; } var index = -1; iteratees = arrayMap(iteratees, baseUnary(getIteratee())); var result = baseMap(collection, function(value, key, collection) { var criteria = arrayMap(iteratees, function(iteratee) { return iteratee(value); }); return { 'criteria': criteria, 'index': ++index, 'value': value }; }); return baseSortBy(result, function(object, other) { return compareMultiple(object, other, orders); }); } /** * The base implementation of `_.pick` without support for individual * property identifiers. * * @private * @param {Object} object The source object. * @param {string[]} paths The property paths to pick. * @returns {Object} Returns the new object. */ function basePick(object, paths) { return basePickBy(object, paths, function(value, path) { return hasIn(object, path); }); } /** * The base implementation of `_.pickBy` without support for iteratee shorthands. * * @private * @param {Object} object The source object. * @param {string[]} paths The property paths to pick. * @param {Function} predicate The function invoked per property. * @returns {Object} Returns the new object. */ function basePickBy(object, paths, predicate) { var index = -1, length = paths.length, result = {}; while (++index < length) { var path = paths[index], value = baseGet(object, path); if (predicate(value, path)) { baseSet(result, castPath(path, object), value); } } return result; } /** * A specialized version of `baseProperty` which supports deep paths. * * @private * @param {Array|string} path The path of the property to get. * @returns {Function} Returns the new accessor function. */ function basePropertyDeep(path) { return function(object) { return baseGet(object, path); }; } /** * The base implementation of `_.pullAllBy` without support for iteratee * shorthands. * * @private * @param {Array} array The array to modify. * @param {Array} values The values to remove. * @param {Function} [iteratee] The iteratee invoked per element. * @param {Function} [comparator] The comparator invoked per element. * @returns {Array} Returns `array`. */ function basePullAll(array, values, iteratee, comparator) { var indexOf = comparator ? baseIndexOfWith : baseIndexOf, index = -1, length = values.length, seen = array; if (array === values) { values = copyArray(values); } if (iteratee) { seen = arrayMap(array, baseUnary(iteratee)); } while (++index < length) { var fromIndex = 0, value = values[index], computed = iteratee ? iteratee(value) : value; while ((fromIndex = indexOf(seen, computed, fromIndex, comparator)) > -1) { if (seen !== array) { splice.call(seen, fromIndex, 1); } splice.call(array, fromIndex, 1); } } return array; } /** * The base implementation of `_.pullAt` without support for individual * indexes or capturing the removed elements. * * @private * @param {Array} array The array to modify. * @param {number[]} indexes The indexes of elements to remove. * @returns {Array} Returns `array`. */ function basePullAt(array, indexes) { var length = array ? indexes.length : 0, lastIndex = length - 1; while (length--) { var index = indexes[length]; if (length == lastIndex || index !== previous) { var previous = index; if (isIndex(index)) { splice.call(array, index, 1); } else { baseUnset(array, index); } } } return array; } /** * The base implementation of `_.random` without support for returning * floating-point numbers. * * @private * @param {number} lower The lower bound. * @param {number} upper The upper bound. * @returns {number} Returns the random number. */ function baseRandom(lower, upper) { return lower + nativeFloor(nativeRandom() * (upper - lower + 1)); } /** * The base implementation of `_.range` and `_.rangeRight` which doesn't * coerce arguments. * * @private * @param {number} start The start of the range. * @param {number} end The end of the range. * @param {number} step The value to increment or decrement by. * @param {boolean} [fromRight] Specify iterating from right to left. * @returns {Array} Returns the range of numbers. */ function baseRange(start, end, step, fromRight) { var index = -1, length = nativeMax(nativeCeil((end - start) / (step || 1)), 0), result = Array(length); while (length--) { result[fromRight ? length : ++index] = start; start += step; } return result; } /** * The base implementation of `_.repeat` which doesn't coerce arguments. * * @private * @param {string} string The string to repeat. * @param {number} n The number of times to repeat the string. * @returns {string} Returns the repeated string. */ function baseRepeat(string, n) { var result = ''; if (!string || n < 1 || n > MAX_SAFE_INTEGER) { return result; } // Leverage the exponentiation by squaring algorithm for a faster repeat. // See https://en.wikipedia.org/wiki/Exponentiation_by_squaring for more details. do { if (n % 2) { result += string; } n = nativeFloor(n / 2); if (n) { string += string; } } while (n); return result; } /** * The base implementation of `_.rest` which doesn't validate or coerce arguments. * * @private * @param {Function} func The function to apply a rest parameter to. * @param {number} [start=func.length-1] The start position of the rest parameter. * @returns {Function} Returns the new function. */ function baseRest(func, start) { return setToString(overRest(func, start, identity), func + ''); } /** * The base implementation of `_.sample`. * * @private * @param {Array|Object} collection The collection to sample. * @returns {*} Returns the random element. */ function baseSample(collection) { return arraySample(values(collection)); } /** * The base implementation of `_.sampleSize` without param guards. * * @private * @param {Array|Object} collection The collection to sample. * @param {number} n The number of elements to sample. * @returns {Array} Returns the random elements. */ function baseSampleSize(collection, n) { var array = values(collection); return shuffleSelf(array, baseClamp(n, 0, array.length)); } /** * The base implementation of `_.set`. * * @private * @param {Object} object The object to modify. * @param {Array|string} path The path of the property to set. * @param {*} value The value to set. * @param {Function} [customizer] The function to customize path creation. * @returns {Object} Returns `object`. */ function baseSet(object, path, value, customizer) { if (!isObject(object)) { return object; } path = castPath(path, object); var index = -1, length = path.length, lastIndex = length - 1, nested = object; while (nested != null && ++index < length) { var key = toKey(path[index]), newValue = value; if (key === '__proto__' || key === 'constructor' || key === 'prototype') { return object; } if (index != lastIndex) { var objValue = nested[key]; newValue = customizer ? customizer(objValue, key, nested) : undefined; if (newValue === undefined) { newValue = isObject(objValue) ? objValue : (isIndex(path[index + 1]) ? [] : {}); } } assignValue(nested, key, newValue); nested = nested[key]; } return object; } /** * The base implementation of `setData` without support for hot loop shorting. * * @private * @param {Function} func The function to associate metadata with. * @param {*} data The metadata. * @returns {Function} Returns `func`. */ var baseSetData = !metaMap ? identity : function(func, data) { metaMap.set(func, data); return func; }; /** * The base implementation of `setToString` without support for hot loop shorting. * * @private * @param {Function} func The function to modify. * @param {Function} string The `toString` result. * @returns {Function} Returns `func`. */ var baseSetToString = !defineProperty ? identity : function(func, string) { return defineProperty(func, 'toString', { 'configurable': true, 'enumerable': false, 'value': constant(string), 'writable': true }); }; /** * The base implementation of `_.shuffle`. * * @private * @param {Array|Object} collection The collection to shuffle. * @returns {Array} Returns the new shuffled array. */ function baseShuffle(collection) { return shuffleSelf(values(collection)); } /** * The base implementation of `_.slice` without an iteratee call guard. * * @private * @param {Array} array The array to slice. * @param {number} [start=0] The start position. * @param {number} [end=array.length] The end position. * @returns {Array} Returns the slice of `array`. */ function baseSlice(array, start, end) { var index = -1, length = array.length; if (start < 0) { start = -start > length ? 0 : (length + start); } end = end > length ? length : end; if (end < 0) { end += length; } length = start > end ? 0 : ((end - start) >>> 0); start >>>= 0; var result = Array(length); while (++index < length) { result[index] = array[index + start]; } return result; } /** * The base implementation of `_.some` without support for iteratee shorthands. * * @private * @param {Array|Object} collection The collection to iterate over. * @param {Function} predicate The function invoked per iteration. * @returns {boolean} Returns `true` if any element passes the predicate check, * else `false`. */ function baseSome(collection, predicate) { var result; baseEach(collection, function(value, index, collection) { result = predicate(value, index, collection); return !result; }); return !!result; } /** * The base implementation of `_.sortedIndex` and `_.sortedLastIndex` which * performs a binary search of `array` to determine the index at which `value` * should be inserted into `array` in order to maintain its sort order. * * @private * @param {Array} array The sorted array to inspect. * @param {*} value The value to evaluate. * @param {boolean} [retHighest] Specify returning the highest qualified index. * @returns {number} Returns the index at which `value` should be inserted * into `array`. */ function baseSortedIndex(array, value, retHighest) { var low = 0, high = array == null ? low : array.length; if (typeof value == 'number' && value === value && high <= HALF_MAX_ARRAY_LENGTH) { while (low < high) { var mid = (low + high) >>> 1, computed = array[mid]; if (computed !== null && !isSymbol(computed) && (retHighest ? (computed <= value) : (computed < value))) { low = mid + 1; } else { high = mid; } } return high; } return baseSortedIndexBy(array, value, identity, retHighest); } /** * The base implementation of `_.sortedIndexBy` and `_.sortedLastIndexBy` * which invokes `iteratee` for `value` and each element of `array` to compute * their sort ranking. The iteratee is invoked with one argument; (value). * * @private * @param {Array} array The sorted array to inspect. * @param {*} value The value to evaluate. * @param {Function} iteratee The iteratee invoked per element. * @param {boolean} [retHighest] Specify returning the highest qualified index. * @returns {number} Returns the index at which `value` should be inserted * into `array`. */ function baseSortedIndexBy(array, value, iteratee, retHighest) { var low = 0, high = array == null ? 0 : array.length; if (high === 0) { return 0; } value = iteratee(value); var valIsNaN = value !== value, valIsNull = value === null, valIsSymbol = isSymbol(value), valIsUndefined = value === undefined; while (low < high) { var mid = nativeFloor((low + high) / 2), computed = iteratee(array[mid]), othIsDefined = computed !== undefined, othIsNull = computed === null, othIsReflexive = computed === computed, othIsSymbol = isSymbol(computed); if (valIsNaN) { var setLow = retHighest || othIsReflexive; } else if (valIsUndefined) { setLow = othIsReflexive && (retHighest || othIsDefined); } else if (valIsNull) { setLow = othIsReflexive && othIsDefined && (retHighest || !othIsNull); } else if (valIsSymbol) { setLow = othIsReflexive && othIsDefined && !othIsNull && (retHighest || !othIsSymbol); } else if (othIsNull || othIsSymbol) { setLow = false; } else { setLow = retHighest ? (computed <= value) : (computed < value); } if (setLow) { low = mid + 1; } else { high = mid; } } return nativeMin(high, MAX_ARRAY_INDEX); } /** * The base implementation of `_.sortedUniq` and `_.sortedUniqBy` without * support for iteratee shorthands. * * @private * @param {Array} array The array to inspect. * @param {Function} [iteratee] The iteratee invoked per element. * @returns {Array} Returns the new duplicate free array. */ function baseSortedUniq(array, iteratee) { var index = -1, length = array.length, resIndex = 0, result = []; while (++index < length) { var value = array[index], computed = iteratee ? iteratee(value) : value; if (!index || !eq(computed, seen)) { var seen = computed; result[resIndex++] = value === 0 ? 0 : value; } } return result; } /** * The base implementation of `_.toNumber` which doesn't ensure correct * conversions of binary, hexadecimal, or octal string values. * * @private * @param {*} value The value to process. * @returns {number} Returns the number. */ function baseToNumber(value) { if (typeof value == 'number') { return value; } if (isSymbol(value)) { return NAN; } return +value; } /** * The base implementation of `_.toString` which doesn't convert nullish * values to empty strings. * * @private * @param {*} value The value to process. * @returns {string} Returns the string. */ function baseToString(value) { // Exit early for strings to avoid a performance hit in some environments. if (typeof value == 'string') { return value; } if (isArray(value)) { // Recursively convert values (susceptible to call stack limits). return arrayMap(value, baseToString) + ''; } if (isSymbol(value)) { return symbolToString ? symbolToString.call(value) : ''; } var result = (value + ''); return (result == '0' && (1 / value) == -INFINITY) ? '-0' : result; } /** * The base implementation of `_.uniqBy` without support for iteratee shorthands. * * @private * @param {Array} array The array to inspect. * @param {Function} [iteratee] The iteratee invoked per element. * @param {Function} [comparator] The comparator invoked per element. * @returns {Array} Returns the new duplicate free array. */ function baseUniq(array, iteratee, comparator) { var index = -1, includes = arrayIncludes, length = array.length, isCommon = true, result = [], seen = result; if (comparator) { isCommon = false; includes = arrayIncludesWith; } else if (length >= LARGE_ARRAY_SIZE) { var set = iteratee ? null : createSet(array); if (set) { return setToArray(set); } isCommon = false; includes = cacheHas; seen = new SetCache; } else { seen = iteratee ? [] : result; } outer: while (++index < length) { var value = array[index], computed = iteratee ? iteratee(value) : value; value = (comparator || value !== 0) ? value : 0; if (isCommon && computed === computed) { var seenIndex = seen.length; while (seenIndex--) { if (seen[seenIndex] === computed) { continue outer; } } if (iteratee) { seen.push(computed); } result.push(value); } else if (!includes(seen, computed, comparator)) { if (seen !== result) { seen.push(computed); } result.push(value); } } return result; } /** * The base implementation of `_.unset`. * * @private * @param {Object} object The object to modify. * @param {Array|string} path The property path to unset. * @returns {boolean} Returns `true` if the property is deleted, else `false`. */ function baseUnset(object, path) { path = castPath(path, object); object = parent(object, path); return object == null || delete object[toKey(last(path))]; } /** * The base implementation of `_.update`. * * @private * @param {Object} object The object to modify. * @param {Array|string} path The path of the property to update. * @param {Function} updater The function to produce the updated value. * @param {Function} [customizer] The function to customize path creation. * @returns {Object} Returns `object`. */ function baseUpdate(object, path, updater, customizer) { return baseSet(object, path, updater(baseGet(object, path)), customizer); } /** * The base implementation of methods like `_.dropWhile` and `_.takeWhile` * without support for iteratee shorthands. * * @private * @param {Array} array The array to query. * @param {Function} predicate The function invoked per iteration. * @param {boolean} [isDrop] Specify dropping elements instead of taking them. * @param {boolean} [fromRight] Specify iterating from right to left. * @returns {Array} Returns the slice of `array`. */ function baseWhile(array, predicate, isDrop, fromRight) { var length = array.length, index = fromRight ? length : -1; while ((fromRight ? index-- : ++index < length) && predicate(array[index], index, array)) {} return isDrop ? baseSlice(array, (fromRight ? 0 : index), (fromRight ? index + 1 : length)) : baseSlice(array, (fromRight ? index + 1 : 0), (fromRight ? length : index)); } /** * The base implementation of `wrapperValue` which returns the result of * performing a sequence of actions on the unwrapped `value`, where each * successive action is supplied the return value of the previous. * * @private * @param {*} value The unwrapped value. * @param {Array} actions Actions to perform to resolve the unwrapped value. * @returns {*} Returns the resolved value. */ function baseWrapperValue(value, actions) { var result = value; if (result instanceof LazyWrapper) { result = result.value(); } return arrayReduce(actions, function(result, action) { return action.func.apply(action.thisArg, arrayPush([result], action.args)); }, result); } /** * The base implementation of methods like `_.xor`, without support for * iteratee shorthands, that accepts an array of arrays to inspect. * * @private * @param {Array} arrays The arrays to inspect. * @param {Function} [iteratee] The iteratee invoked per element. * @param {Function} [comparator] The comparator invoked per element. * @returns {Array} Returns the new array of values. */ function baseXor(arrays, iteratee, comparator) { var length = arrays.length; if (length < 2) { return length ? baseUniq(arrays[0]) : []; } var index = -1, result = Array(length); while (++index < length) { var array = arrays[index], othIndex = -1; while (++othIndex < length) { if (othIndex != index) { result[index] = baseDifference(result[index] || array, arrays[othIndex], iteratee, comparator); } } } return baseUniq(baseFlatten(result, 1), iteratee, comparator); } /** * This base implementation of `_.zipObject` which assigns values using `assignFunc`. * * @private * @param {Array} props The property identifiers. * @param {Array} values The property values. * @param {Function} assignFunc The function to assign values. * @returns {Object} Returns the new object. */ function baseZipObject(props, values, assignFunc) { var index = -1, length = props.length, valsLength = values.length, result = {}; while (++index < length) { var value = index < valsLength ? values[index] : undefined; assignFunc(result, props[index], value); } return result; } /** * Casts `value` to an empty array if it's not an array like object. * * @private * @param {*} value The value to inspect. * @returns {Array|Object} Returns the cast array-like object. */ function castArrayLikeObject(value) { return isArrayLikeObject(value) ? value : []; } /** * Casts `value` to `identity` if it's not a function. * * @private * @param {*} value The value to inspect. * @returns {Function} Returns cast function. */ function castFunction(value) { return typeof value == 'function' ? value : identity; } /** * Casts `value` to a path array if it's not one. * * @private * @param {*} value The value to inspect. * @param {Object} [object] The object to query keys on. * @returns {Array} Returns the cast property path array. */ function castPath(value, object) { if (isArray(value)) { return value; } return isKey(value, object) ? [value] : stringToPath(toString(value)); } /** * A `baseRest` alias which can be replaced with `identity` by module * replacement plugins. * * @private * @type {Function} * @param {Function} func The function to apply a rest parameter to. * @returns {Function} Returns the new function. */ var castRest = baseRest; /** * Casts `array` to a slice if it's needed. * * @private * @param {Array} array The array to inspect. * @param {number} start The start position. * @param {number} [end=array.length] The end position. * @returns {Array} Returns the cast slice. */ function castSlice(array, start, end) { var length = array.length; end = end === undefined ? length : end; return (!start && end >= length) ? array : baseSlice(array, start, end); } /** * A simple wrapper around the global [`clearTimeout`](https://mdn.io/clearTimeout). * * @private * @param {number|Object} id The timer id or timeout object of the timer to clear. */ var clearTimeout = ctxClearTimeout || function(id) { return root.clearTimeout(id); }; /** * Creates a clone of `buffer`. * * @private * @param {Buffer} buffer The buffer to clone. * @param {boolean} [isDeep] Specify a deep clone. * @returns {Buffer} Returns the cloned buffer. */ function cloneBuffer(buffer, isDeep) { if (isDeep) { return buffer.slice(); } var length = buffer.length, result = allocUnsafe ? allocUnsafe(length) : new buffer.constructor(length); buffer.copy(result); return result; } /** * Creates a clone of `arrayBuffer`. * * @private * @param {ArrayBuffer} arrayBuffer The array buffer to clone. * @returns {ArrayBuffer} Returns the cloned array buffer. */ function cloneArrayBuffer(arrayBuffer) { var result = new arrayBuffer.constructor(arrayBuffer.byteLength); new Uint8Array(result).set(new Uint8Array(arrayBuffer)); return result; } /** * Creates a clone of `dataView`. * * @private * @param {Object} dataView The data view to clone. * @param {boolean} [isDeep] Specify a deep clone. * @returns {Object} Returns the cloned data view. */ function cloneDataView(dataView, isDeep) { var buffer = isDeep ? cloneArrayBuffer(dataView.buffer) : dataView.buffer; return new dataView.constructor(buffer, dataView.byteOffset, dataView.byteLength); } /** * Creates a clone of `regexp`. * * @private * @param {Object} regexp The regexp to clone. * @returns {Object} Returns the cloned regexp. */ function cloneRegExp(regexp) { var result = new regexp.constructor(regexp.source, reFlags.exec(regexp)); result.lastIndex = regexp.lastIndex; return result; } /** * Creates a clone of the `symbol` object. * * @private * @param {Object} symbol The symbol object to clone. * @returns {Object} Returns the cloned symbol object. */ function cloneSymbol(symbol) { return symbolValueOf ? Object(symbolValueOf.call(symbol)) : {}; } /** * Creates a clone of `typedArray`. * * @private * @param {Object} typedArray The typed array to clone. * @param {boolean} [isDeep] Specify a deep clone. * @returns {Object} Returns the cloned typed array. */ function cloneTypedArray(typedArray, isDeep) { var buffer = isDeep ? cloneArrayBuffer(typedArray.buffer) : typedArray.buffer; return new typedArray.constructor(buffer, typedArray.byteOffset, typedArray.length); } /** * Compares values to sort them in ascending order. * * @private * @param {*} value The value to compare. * @param {*} other The other value to compare. * @returns {number} Returns the sort order indicator for `value`. */ function compareAscending(value, other) { if (value !== other) { var valIsDefined = value !== undefined, valIsNull = value === null, valIsReflexive = value === value, valIsSymbol = isSymbol(value); var othIsDefined = other !== undefined, othIsNull = other === null, othIsReflexive = other === other, othIsSymbol = isSymbol(other); if ((!othIsNull && !othIsSymbol && !valIsSymbol && value > other) || (valIsSymbol && othIsDefined && othIsReflexive && !othIsNull && !othIsSymbol) || (valIsNull && othIsDefined && othIsReflexive) || (!valIsDefined && othIsReflexive) || !valIsReflexive) { return 1; } if ((!valIsNull && !valIsSymbol && !othIsSymbol && value < other) || (othIsSymbol && valIsDefined && valIsReflexive && !valIsNull && !valIsSymbol) || (othIsNull && valIsDefined && valIsReflexive) || (!othIsDefined && valIsReflexive) || !othIsReflexive) { return -1; } } return 0; } /** * Used by `_.orderBy` to compare multiple properties of a value to another * and stable sort them. * * If `orders` is unspecified, all values are sorted in ascending order. Otherwise, * specify an order of "desc" for descending or "asc" for ascending sort order * of corresponding values. * * @private * @param {Object} object The object to compare. * @param {Object} other The other object to compare. * @param {boolean[]|string[]} orders The order to sort by for each property. * @returns {number} Returns the sort order indicator for `object`. */ function compareMultiple(object, other, orders) { var index = -1, objCriteria = object.criteria, othCriteria = other.criteria, length = objCriteria.length, ordersLength = orders.length; while (++index < length) { var result = compareAscending(objCriteria[index], othCriteria[index]); if (result) { if (index >= ordersLength) { return result; } var order = orders[index]; return result * (order == 'desc' ? -1 : 1); } } // Fixes an `Array#sort` bug in the JS engine embedded in Adobe applications // that causes it, under certain circumstances, to provide the same value for // `object` and `other`. See https://github.com/jashkenas/underscore/pull/1247 // for more details. // // This also ensures a stable sort in V8 and other engines. // See https://bugs.chromium.org/p/v8/issues/detail?id=90 for more details. return object.index - other.index; } /** * Creates an array that is the composition of partially applied arguments, * placeholders, and provided arguments into a single array of arguments. * * @private * @param {Array} args The provided arguments. * @param {Array} partials The arguments to prepend to those provided. * @param {Array} holders The `partials` placeholder indexes. * @params {boolean} [isCurried] Specify composing for a curried function. * @returns {Array} Returns the new array of composed arguments. */ function composeArgs(args, partials, holders, isCurried) { var argsIndex = -1, argsLength = args.length, holdersLength = holders.length, leftIndex = -1, leftLength = partials.length, rangeLength = nativeMax(argsLength - holdersLength, 0), result = Array(leftLength + rangeLength), isUncurried = !isCurried; while (++leftIndex < leftLength) { result[leftIndex] = partials[leftIndex]; } while (++argsIndex < holdersLength) { if (isUncurried || argsIndex < argsLength) { result[holders[argsIndex]] = args[argsIndex]; } } while (rangeLength--) { result[leftIndex++] = args[argsIndex++]; } return result; } /** * This function is like `composeArgs` except that the arguments composition * is tailored for `_.partialRight`. * * @private * @param {Array} args The provided arguments. * @param {Array} partials The arguments to append to those provided. * @param {Array} holders The `partials` placeholder indexes. * @params {boolean} [isCurried] Specify composing for a curried function. * @returns {Array} Returns the new array of composed arguments. */ function composeArgsRight(args, partials, holders, isCurried) { var argsIndex = -1, argsLength = args.length, holdersIndex = -1, holdersLength = holders.length, rightIndex = -1, rightLength = partials.length, rangeLength = nativeMax(argsLength - holdersLength, 0), result = Array(rangeLength + rightLength), isUncurried = !isCurried; while (++argsIndex < rangeLength) { result[argsIndex] = args[argsIndex]; } var offset = argsIndex; while (++rightIndex < rightLength) { result[offset + rightIndex] = partials[rightIndex]; } while (++holdersIndex < holdersLength) { if (isUncurried || argsIndex < argsLength) { result[offset + holders[holdersIndex]] = args[argsIndex++]; } } return result; } /** * Copies the values of `source` to `array`. * * @private * @param {Array} source The array to copy values from. * @param {Array} [array=[]] The array to copy values to. * @returns {Array} Returns `array`. */ function copyArray(source, array) { var index = -1, length = source.length; array || (array = Array(length)); while (++index < length) { array[index] = source[index]; } return array; } /** * Copies properties of `source` to `object`. * * @private * @param {Object} source The object to copy properties from. * @param {Array} props The property identifiers to copy. * @param {Object} [object={}] The object to copy properties to. * @param {Function} [customizer] The function to customize copied values. * @returns {Object} Returns `object`. */ function copyObject(source, props, object, customizer) { var isNew = !object; object || (object = {}); var index = -1, length = props.length; while (++index < length) { var key = props[index]; var newValue = customizer ? customizer(object[key], source[key], key, object, source) : undefined; if (newValue === undefined) { newValue = source[key]; } if (isNew) { baseAssignValue(object, key, newValue); } else { assignValue(object, key, newValue); } } return object; } /** * Copies own symbols of `source` to `object`. * * @private * @param {Object} source The object to copy symbols from. * @param {Object} [object={}] The object to copy symbols to. * @returns {Object} Returns `object`. */ function copySymbols(source, object) { return copyObject(source, getSymbols(source), object); } /** * Copies own and inherited symbols of `source` to `object`. * * @private * @param {Object} source The object to copy symbols from. * @param {Object} [object={}] The object to copy symbols to. * @returns {Object} Returns `object`. */ function copySymbolsIn(source, object) { return copyObject(source, getSymbolsIn(source), object); } /** * Creates a function like `_.groupBy`. * * @private * @param {Function} setter The function to set accumulator values. * @param {Function} [initializer] The accumulator object initializer. * @returns {Function} Returns the new aggregator function. */ function createAggregator(setter, initializer) { return function(collection, iteratee) { var func = isArray(collection) ? arrayAggregator : baseAggregator, accumulator = initializer ? initializer() : {}; return func(collection, setter, getIteratee(iteratee, 2), accumulator); }; } /** * Creates a function like `_.assign`. * * @private * @param {Function} assigner The function to assign values. * @returns {Function} Returns the new assigner function. */ function createAssigner(assigner) { return baseRest(function(object, sources) { var index = -1, length = sources.length, customizer = length > 1 ? sources[length - 1] : undefined, guard = length > 2 ? sources[2] : undefined; customizer = (assigner.length > 3 && typeof customizer == 'function') ? (length--, customizer) : undefined; if (guard && isIterateeCall(sources[0], sources[1], guard)) { customizer = length < 3 ? undefined : customizer; length = 1; } object = Object(object); while (++index < length) { var source = sources[index]; if (source) { assigner(object, source, index, customizer); } } return object; }); } /** * Creates a `baseEach` or `baseEachRight` function. * * @private * @param {Function} eachFunc The function to iterate over a collection. * @param {boolean} [fromRight] Specify iterating from right to left. * @returns {Function} Returns the new base function. */ function createBaseEach(eachFunc, fromRight) { return function(collection, iteratee) { if (collection == null) { return collection; } if (!isArrayLike(collection)) { return eachFunc(collection, iteratee); } var length = collection.length, index = fromRight ? length : -1, iterable = Object(collection); while ((fromRight ? index-- : ++index < length)) { if (iteratee(iterable[index], index, iterable) === false) { break; } } return collection; }; } /** * Creates a base function for methods like `_.forIn` and `_.forOwn`. * * @private * @param {boolean} [fromRight] Specify iterating from right to left. * @returns {Function} Returns the new base function. */ function createBaseFor(fromRight) { return function(object, iteratee, keysFunc) { var index = -1, iterable = Object(object), props = keysFunc(object), length = props.length; while (length--) { var key = props[fromRight ? length : ++index]; if (iteratee(iterable[key], key, iterable) === false) { break; } } return object; }; } /** * Creates a function that wraps `func` to invoke it with the optional `this` * binding of `thisArg`. * * @private * @param {Function} func The function to wrap. * @param {number} bitmask The bitmask flags. See `createWrap` for more details. * @param {*} [thisArg] The `this` binding of `func`. * @returns {Function} Returns the new wrapped function. */ function createBind(func, bitmask, thisArg) { var isBind = bitmask & WRAP_BIND_FLAG, Ctor = createCtor(func); function wrapper() { var fn = (this && this !== root && this instanceof wrapper) ? Ctor : func; return fn.apply(isBind ? thisArg : this, arguments); } return wrapper; } /** * Creates a function like `_.lowerFirst`. * * @private * @param {string} methodName The name of the `String` case method to use. * @returns {Function} Returns the new case function. */ function createCaseFirst(methodName) { return function(string) { string = toString(string); var strSymbols = hasUnicode(string) ? stringToArray(string) : undefined; var chr = strSymbols ? strSymbols[0] : string.charAt(0); var trailing = strSymbols ? castSlice(strSymbols, 1).join('') : string.slice(1); return chr[methodName]() + trailing; }; } /** * Creates a function like `_.camelCase`. * * @private * @param {Function} callback The function to combine each word. * @returns {Function} Returns the new compounder function. */ function createCompounder(callback) { return function(string) { return arrayReduce(words(deburr(string).replace(reApos, '')), callback, ''); }; } /** * Creates a function that produces an instance of `Ctor` regardless of * whether it was invoked as part of a `new` expression or by `call` or `apply`. * * @private * @param {Function} Ctor The constructor to wrap. * @returns {Function} Returns the new wrapped function. */ function createCtor(Ctor) { return function() { // Use a `switch` statement to work with class constructors. See // http://ecma-international.org/ecma-262/7.0/#sec-ecmascript-function-objects-call-thisargument-argumentslist // for more details. var args = arguments; switch (args.length) { case 0: return new Ctor; case 1: return new Ctor(args[0]); case 2: return new Ctor(args[0], args[1]); case 3: return new Ctor(args[0], args[1], args[2]); case 4: return new Ctor(args[0], args[1], args[2], args[3]); case 5: return new Ctor(args[0], args[1], args[2], args[3], args[4]); case 6: return new Ctor(args[0], args[1], args[2], args[3], args[4], args[5]); case 7: return new Ctor(args[0], args[1], args[2], args[3], args[4], args[5], args[6]); } var thisBinding = baseCreate(Ctor.prototype), result = Ctor.apply(thisBinding, args); // Mimic the constructor's `return` behavior. // See https://es5.github.io/#x13.2.2 for more details. return isObject(result) ? result : thisBinding; }; } /** * Creates a function that wraps `func` to enable currying. * * @private * @param {Function} func The function to wrap. * @param {number} bitmask The bitmask flags. See `createWrap` for more details. * @param {number} arity The arity of `func`. * @returns {Function} Returns the new wrapped function. */ function createCurry(func, bitmask, arity) { var Ctor = createCtor(func); function wrapper() { var length = arguments.length, args = Array(length), index = length, placeholder = getHolder(wrapper); while (index--) { args[index] = arguments[index]; } var holders = (length < 3 && args[0] !== placeholder && args[length - 1] !== placeholder) ? [] : replaceHolders(args, placeholder); length -= holders.length; if (length < arity) { return createRecurry( func, bitmask, createHybrid, wrapper.placeholder, undefined, args, holders, undefined, undefined, arity - length); } var fn = (this && this !== root && this instanceof wrapper) ? Ctor : func; return apply(fn, this, args); } return wrapper; } /** * Creates a `_.find` or `_.findLast` function. * * @private * @param {Function} findIndexFunc The function to find the collection index. * @returns {Function} Returns the new find function. */ function createFind(findIndexFunc) { return function(collection, predicate, fromIndex) { var iterable = Object(collection); if (!isArrayLike(collection)) { var iteratee = getIteratee(predicate, 3); collection = keys(collection); predicate = function(key) { return iteratee(iterable[key], key, iterable); }; } var index = findIndexFunc(collection, predicate, fromIndex); return index > -1 ? iterable[iteratee ? collection[index] : index] : undefined; }; } /** * Creates a `_.flow` or `_.flowRight` function. * * @private * @param {boolean} [fromRight] Specify iterating from right to left. * @returns {Function} Returns the new flow function. */ function createFlow(fromRight) { return flatRest(function(funcs) { var length = funcs.length, index = length, prereq = LodashWrapper.prototype.thru; if (fromRight) { funcs.reverse(); } while (index--) { var func = funcs[index]; if (typeof func != 'function') { throw new TypeError(FUNC_ERROR_TEXT); } if (prereq && !wrapper && getFuncName(func) == 'wrapper') { var wrapper = new LodashWrapper([], true); } } index = wrapper ? index : length; while (++index < length) { func = funcs[index]; var funcName = getFuncName(func), data = funcName == 'wrapper' ? getData(func) : undefined; if (data && isLaziable(data[0]) && data[1] == (WRAP_ARY_FLAG | WRAP_CURRY_FLAG | WRAP_PARTIAL_FLAG | WRAP_REARG_FLAG) && !data[4].length && data[9] == 1 ) { wrapper = wrapper[getFuncName(data[0])].apply(wrapper, data[3]); } else { wrapper = (func.length == 1 && isLaziable(func)) ? wrapper[funcName]() : wrapper.thru(func); } } return function() { var args = arguments, value = args[0]; if (wrapper && args.length == 1 && isArray(value)) { return wrapper.plant(value).value(); } var index = 0, result = length ? funcs[index].apply(this, args) : value; while (++index < length) { result = funcs[index].call(this, result); } return result; }; }); } /** * Creates a function that wraps `func` to invoke it with optional `this` * binding of `thisArg`, partial application, and currying. * * @private * @param {Function|string} func The function or method name to wrap. * @param {number} bitmask The bitmask flags. See `createWrap` for more details. * @param {*} [thisArg] The `this` binding of `func`. * @param {Array} [partials] The arguments to prepend to those provided to * the new function. * @param {Array} [holders] The `partials` placeholder indexes. * @param {Array} [partialsRight] The arguments to append to those provided * to the new function. * @param {Array} [holdersRight] The `partialsRight` placeholder indexes. * @param {Array} [argPos] The argument positions of the new function. * @param {number} [ary] The arity cap of `func`. * @param {number} [arity] The arity of `func`. * @returns {Function} Returns the new wrapped function. */ function createHybrid(func, bitmask, thisArg, partials, holders, partialsRight, holdersRight, argPos, ary, arity) { var isAry = bitmask & WRAP_ARY_FLAG, isBind = bitmask & WRAP_BIND_FLAG, isBindKey = bitmask & WRAP_BIND_KEY_FLAG, isCurried = bitmask & (WRAP_CURRY_FLAG | WRAP_CURRY_RIGHT_FLAG), isFlip = bitmask & WRAP_FLIP_FLAG, Ctor = isBindKey ? undefined : createCtor(func); function wrapper() { var length = arguments.length, args = Array(length), index = length; while (index--) { args[index] = arguments[index]; } if (isCurried) { var placeholder = getHolder(wrapper), holdersCount = countHolders(args, placeholder); } if (partials) { args = composeArgs(args, partials, holders, isCurried); } if (partialsRight) { args = composeArgsRight(args, partialsRight, holdersRight, isCurried); } length -= holdersCount; if (isCurried && length < arity) { var newHolders = replaceHolders(args, placeholder); return createRecurry( func, bitmask, createHybrid, wrapper.placeholder, thisArg, args, newHolders, argPos, ary, arity - length ); } var thisBinding = isBind ? thisArg : this, fn = isBindKey ? thisBinding[func] : func; length = args.length; if (argPos) { args = reorder(args, argPos); } else if (isFlip && length > 1) { args.reverse(); } if (isAry && ary < length) { args.length = ary; } if (this && this !== root && this instanceof wrapper) { fn = Ctor || createCtor(fn); } return fn.apply(thisBinding, args); } return wrapper; } /** * Creates a function like `_.invertBy`. * * @private * @param {Function} setter The function to set accumulator values. * @param {Function} toIteratee The function to resolve iteratees. * @returns {Function} Returns the new inverter function. */ function createInverter(setter, toIteratee) { return function(object, iteratee) { return baseInverter(object, setter, toIteratee(iteratee), {}); }; } /** * Creates a function that performs a mathematical operation on two values. * * @private * @param {Function} operator The function to perform the operation. * @param {number} [defaultValue] The value used for `undefined` arguments. * @returns {Function} Returns the new mathematical operation function. */ function createMathOperation(operator, defaultValue) { return function(value, other) { var result; if (value === undefined && other === undefined) { return defaultValue; } if (value !== undefined) { result = value; } if (other !== undefined) { if (result === undefined) { return other; } if (typeof value == 'string' || typeof other == 'string') { value = baseToString(value); other = baseToString(other); } else { value = baseToNumber(value); other = baseToNumber(other); } result = operator(value, other); } return result; }; } /** * Creates a function like `_.over`. * * @private * @param {Function} arrayFunc The function to iterate over iteratees. * @returns {Function} Returns the new over function. */ function createOver(arrayFunc) { return flatRest(function(iteratees) { iteratees = arrayMap(iteratees, baseUnary(getIteratee())); return baseRest(function(args) { var thisArg = this; return arrayFunc(iteratees, function(iteratee) { return apply(iteratee, thisArg, args); }); }); }); } /** * Creates the padding for `string` based on `length`. The `chars` string * is truncated if the number of characters exceeds `length`. * * @private * @param {number} length The padding length. * @param {string} [chars=' '] The string used as padding. * @returns {string} Returns the padding for `string`. */ function createPadding(length, chars) { chars = chars === undefined ? ' ' : baseToString(chars); var charsLength = chars.length; if (charsLength < 2) { return charsLength ? baseRepeat(chars, length) : chars; } var result = baseRepeat(chars, nativeCeil(length / stringSize(chars))); return hasUnicode(chars) ? castSlice(stringToArray(result), 0, length).join('') : result.slice(0, length); } /** * Creates a function that wraps `func` to invoke it with the `this` binding * of `thisArg` and `partials` prepended to the arguments it receives. * * @private * @param {Function} func The function to wrap. * @param {number} bitmask The bitmask flags. See `createWrap` for more details. * @param {*} thisArg The `this` binding of `func`. * @param {Array} partials The arguments to prepend to those provided to * the new function. * @returns {Function} Returns the new wrapped function. */ function createPartial(func, bitmask, thisArg, partials) { var isBind = bitmask & WRAP_BIND_FLAG, Ctor = createCtor(func); function wrapper() { var argsIndex = -1, argsLength = arguments.length, leftIndex = -1, leftLength = partials.length, args = Array(leftLength + argsLength), fn = (this && this !== root && this instanceof wrapper) ? Ctor : func; while (++leftIndex < leftLength) { args[leftIndex] = partials[leftIndex]; } while (argsLength--) { args[leftIndex++] = arguments[++argsIndex]; } return apply(fn, isBind ? thisArg : this, args); } return wrapper; } /** * Creates a `_.range` or `_.rangeRight` function. * * @private * @param {boolean} [fromRight] Specify iterating from right to left. * @returns {Function} Returns the new range function. */ function createRange(fromRight) { return function(start, end, step) { if (step && typeof step != 'number' && isIterateeCall(start, end, step)) { end = step = undefined; } // Ensure the sign of `-0` is preserved. start = toFinite(start); if (end === undefined) { end = start; start = 0; } else { end = toFinite(end); } step = step === undefined ? (start < end ? 1 : -1) : toFinite(step); return baseRange(start, end, step, fromRight); }; } /** * Creates a function that performs a relational operation on two values. * * @private * @param {Function} operator The function to perform the operation. * @returns {Function} Returns the new relational operation function. */ function createRelationalOperation(operator) { return function(value, other) { if (!(typeof value == 'string' && typeof other == 'string')) { value = toNumber(value); other = toNumber(other); } return operator(value, other); }; } /** * Creates a function that wraps `func` to continue currying. * * @private * @param {Function} func The function to wrap. * @param {number} bitmask The bitmask flags. See `createWrap` for more details. * @param {Function} wrapFunc The function to create the `func` wrapper. * @param {*} placeholder The placeholder value. * @param {*} [thisArg] The `this` binding of `func`. * @param {Array} [partials] The arguments to prepend to those provided to * the new function. * @param {Array} [holders] The `partials` placeholder indexes. * @param {Array} [argPos] The argument positions of the new function. * @param {number} [ary] The arity cap of `func`. * @param {number} [arity] The arity of `func`. * @returns {Function} Returns the new wrapped function. */ function createRecurry(func, bitmask, wrapFunc, placeholder, thisArg, partials, holders, argPos, ary, arity) { var isCurry = bitmask & WRAP_CURRY_FLAG, newHolders = isCurry ? holders : undefined, newHoldersRight = isCurry ? undefined : holders, newPartials = isCurry ? partials : undefined, newPartialsRight = isCurry ? undefined : partials; bitmask |= (isCurry ? WRAP_PARTIAL_FLAG : WRAP_PARTIAL_RIGHT_FLAG); bitmask &= ~(isCurry ? WRAP_PARTIAL_RIGHT_FLAG : WRAP_PARTIAL_FLAG); if (!(bitmask & WRAP_CURRY_BOUND_FLAG)) { bitmask &= ~(WRAP_BIND_FLAG | WRAP_BIND_KEY_FLAG); } var newData = [ func, bitmask, thisArg, newPartials, newHolders, newPartialsRight, newHoldersRight, argPos, ary, arity ]; var result = wrapFunc.apply(undefined, newData); if (isLaziable(func)) { setData(result, newData); } result.placeholder = placeholder; return setWrapToString(result, func, bitmask); } /** * Creates a function like `_.round`. * * @private * @param {string} methodName The name of the `Math` method to use when rounding. * @returns {Function} Returns the new round function. */ function createRound(methodName) { var func = Math[methodName]; return function(number, precision) { number = toNumber(number); precision = precision == null ? 0 : nativeMin(toInteger(precision), 292); if (precision && nativeIsFinite(number)) { // Shift with exponential notation to avoid floating-point issues. // See [MDN](https://mdn.io/round#Examples) for more details. var pair = (toString(number) + 'e').split('e'), value = func(pair[0] + 'e' + (+pair[1] + precision)); pair = (toString(value) + 'e').split('e'); return +(pair[0] + 'e' + (+pair[1] - precision)); } return func(number); }; } /** * Creates a set object of `values`. * * @private * @param {Array} values The values to add to the set. * @returns {Object} Returns the new set. */ var createSet = !(Set && (1 / setToArray(new Set([,-0]))[1]) == INFINITY) ? noop : function(values) { return new Set(values); }; /** * Creates a `_.toPairs` or `_.toPairsIn` function. * * @private * @param {Function} keysFunc The function to get the keys of a given object. * @returns {Function} Returns the new pairs function. */ function createToPairs(keysFunc) { return function(object) { var tag = getTag(object); if (tag == mapTag) { return mapToArray(object); } if (tag == setTag) { return setToPairs(object); } return baseToPairs(object, keysFunc(object)); }; } /** * Creates a function that either curries or invokes `func` with optional * `this` binding and partially applied arguments. * * @private * @param {Function|string} func The function or method name to wrap. * @param {number} bitmask The bitmask flags. * 1 - `_.bind` * 2 - `_.bindKey` * 4 - `_.curry` or `_.curryRight` of a bound function * 8 - `_.curry` * 16 - `_.curryRight` * 32 - `_.partial` * 64 - `_.partialRight` * 128 - `_.rearg` * 256 - `_.ary` * 512 - `_.flip` * @param {*} [thisArg] The `this` binding of `func`. * @param {Array} [partials] The arguments to be partially applied. * @param {Array} [holders] The `partials` placeholder indexes. * @param {Array} [argPos] The argument positions of the new function. * @param {number} [ary] The arity cap of `func`. * @param {number} [arity] The arity of `func`. * @returns {Function} Returns the new wrapped function. */ function createWrap(func, bitmask, thisArg, partials, holders, argPos, ary, arity) { var isBindKey = bitmask & WRAP_BIND_KEY_FLAG; if (!isBindKey && typeof func != 'function') { throw new TypeError(FUNC_ERROR_TEXT); } var length = partials ? partials.length : 0; if (!length) { bitmask &= ~(WRAP_PARTIAL_FLAG | WRAP_PARTIAL_RIGHT_FLAG); partials = holders = undefined; } ary = ary === undefined ? ary : nativeMax(toInteger(ary), 0); arity = arity === undefined ? arity : toInteger(arity); length -= holders ? holders.length : 0; if (bitmask & WRAP_PARTIAL_RIGHT_FLAG) { var partialsRight = partials, holdersRight = holders; partials = holders = undefined; } var data = isBindKey ? undefined : getData(func); var newData = [ func, bitmask, thisArg, partials, holders, partialsRight, holdersRight, argPos, ary, arity ]; if (data) { mergeData(newData, data); } func = newData[0]; bitmask = newData[1]; thisArg = newData[2]; partials = newData[3]; holders = newData[4]; arity = newData[9] = newData[9] === undefined ? (isBindKey ? 0 : func.length) : nativeMax(newData[9] - length, 0); if (!arity && bitmask & (WRAP_CURRY_FLAG | WRAP_CURRY_RIGHT_FLAG)) { bitmask &= ~(WRAP_CURRY_FLAG | WRAP_CURRY_RIGHT_FLAG); } if (!bitmask || bitmask == WRAP_BIND_FLAG) { var result = createBind(func, bitmask, thisArg); } else if (bitmask == WRAP_CURRY_FLAG || bitmask == WRAP_CURRY_RIGHT_FLAG) { result = createCurry(func, bitmask, arity); } else if ((bitmask == WRAP_PARTIAL_FLAG || bitmask == (WRAP_BIND_FLAG | WRAP_PARTIAL_FLAG)) && !holders.length) { result = createPartial(func, bitmask, thisArg, partials); } else { result = createHybrid.apply(undefined, newData); } var setter = data ? baseSetData : setData; return setWrapToString(setter(result, newData), func, bitmask); } /** * Used by `_.defaults` to customize its `_.assignIn` use to assign properties * of source objects to the destination object for all destination properties * that resolve to `undefined`. * * @private * @param {*} objValue The destination value. * @param {*} srcValue The source value. * @param {string} key The key of the property to assign. * @param {Object} object The parent object of `objValue`. * @returns {*} Returns the value to assign. */ function customDefaultsAssignIn(objValue, srcValue, key, object) { if (objValue === undefined || (eq(objValue, objectProto[key]) && !hasOwnProperty.call(object, key))) { return srcValue; } return objValue; } /** * Used by `_.defaultsDeep` to customize its `_.merge` use to merge source * objects into destination objects that are passed thru. * * @private * @param {*} objValue The destination value. * @param {*} srcValue The source value. * @param {string} key The key of the property to merge. * @param {Object} object The parent object of `objValue`. * @param {Object} source The parent object of `srcValue`. * @param {Object} [stack] Tracks traversed source values and their merged * counterparts. * @returns {*} Returns the value to assign. */ function customDefaultsMerge(objValue, srcValue, key, object, source, stack) { if (isObject(objValue) && isObject(srcValue)) { // Recursively merge objects and arrays (susceptible to call stack limits). stack.set(srcValue, objValue); baseMerge(objValue, srcValue, undefined, customDefaultsMerge, stack); stack['delete'](srcValue); } return objValue; } /** * Used by `_.omit` to customize its `_.cloneDeep` use to only clone plain * objects. * * @private * @param {*} value The value to inspect. * @param {string} key The key of the property to inspect. * @returns {*} Returns the uncloned value or `undefined` to defer cloning to `_.cloneDeep`. */ function customOmitClone(value) { return isPlainObject(value) ? undefined : value; } /** * A specialized version of `baseIsEqualDeep` for arrays with support for * partial deep comparisons. * * @private * @param {Array} array The array to compare. * @param {Array} other The other array to compare. * @param {number} bitmask The bitmask flags. See `baseIsEqual` for more details. * @param {Function} customizer The function to customize comparisons. * @param {Function} equalFunc The function to determine equivalents of values. * @param {Object} stack Tracks traversed `array` and `other` objects. * @returns {boolean} Returns `true` if the arrays are equivalent, else `false`. */ function equalArrays(array, other, bitmask, customizer, equalFunc, stack) { var isPartial = bitmask & COMPARE_PARTIAL_FLAG, arrLength = array.length, othLength = other.length; if (arrLength != othLength && !(isPartial && othLength > arrLength)) { return false; } // Check that cyclic values are equal. var arrStacked = stack.get(array); var othStacked = stack.get(other); if (arrStacked && othStacked) { return arrStacked == other && othStacked == array; } var index = -1, result = true, seen = (bitmask & COMPARE_UNORDERED_FLAG) ? new SetCache : undefined; stack.set(array, other); stack.set(other, array); // Ignore non-index properties. while (++index < arrLength) { var arrValue = array[index], othValue = other[index]; if (customizer) { var compared = isPartial ? customizer(othValue, arrValue, index, other, array, stack) : customizer(arrValue, othValue, index, array, other, stack); } if (compared !== undefined) { if (compared) { continue; } result = false; break; } // Recursively compare arrays (susceptible to call stack limits). if (seen) { if (!arraySome(other, function(othValue, othIndex) { if (!cacheHas(seen, othIndex) && (arrValue === othValue || equalFunc(arrValue, othValue, bitmask, customizer, stack))) { return seen.push(othIndex); } })) { result = false; break; } } else if (!( arrValue === othValue || equalFunc(arrValue, othValue, bitmask, customizer, stack) )) { result = false; break; } } stack['delete'](array); stack['delete'](other); return result; } /** * A specialized version of `baseIsEqualDeep` for comparing objects of * the same `toStringTag`. * * **Note:** This function only supports comparing values with tags of * `Boolean`, `Date`, `Error`, `Number`, `RegExp`, or `String`. * * @private * @param {Object} object The object to compare. * @param {Object} other The other object to compare. * @param {string} tag The `toStringTag` of the objects to compare. * @param {number} bitmask The bitmask flags. See `baseIsEqual` for more details. * @param {Function} customizer The function to customize comparisons. * @param {Function} equalFunc The function to determine equivalents of values. * @param {Object} stack Tracks traversed `object` and `other` objects. * @returns {boolean} Returns `true` if the objects are equivalent, else `false`. */ function equalByTag(object, other, tag, bitmask, customizer, equalFunc, stack) { switch (tag) { case dataViewTag: if ((object.byteLength != other.byteLength) || (object.byteOffset != other.byteOffset)) { return false; } object = object.buffer; other = other.buffer; case arrayBufferTag: if ((object.byteLength != other.byteLength) || !equalFunc(new Uint8Array(object), new Uint8Array(other))) { return false; } return true; case boolTag: case dateTag: case numberTag: // Coerce booleans to `1` or `0` and dates to milliseconds. // Invalid dates are coerced to `NaN`. return eq(+object, +other); case errorTag: return object.name == other.name && object.message == other.message; case regexpTag: case stringTag: // Coerce regexes to strings and treat strings, primitives and objects, // as equal. See http://www.ecma-international.org/ecma-262/7.0/#sec-regexp.prototype.tostring // for more details. return object == (other + ''); case mapTag: var convert = mapToArray; case setTag: var isPartial = bitmask & COMPARE_PARTIAL_FLAG; convert || (convert = setToArray); if (object.size != other.size && !isPartial) { return false; } // Assume cyclic values are equal. var stacked = stack.get(object); if (stacked) { return stacked == other; } bitmask |= COMPARE_UNORDERED_FLAG; // Recursively compare objects (susceptible to call stack limits). stack.set(object, other); var result = equalArrays(convert(object), convert(other), bitmask, customizer, equalFunc, stack); stack['delete'](object); return result; case symbolTag: if (symbolValueOf) { return symbolValueOf.call(object) == symbolValueOf.call(other); } } return false; } /** * A specialized version of `baseIsEqualDeep` for objects with support for * partial deep comparisons. * * @private * @param {Object} object The object to compare. * @param {Object} other The other object to compare. * @param {number} bitmask The bitmask flags. See `baseIsEqual` for more details. * @param {Function} customizer The function to customize comparisons. * @param {Function} equalFunc The function to determine equivalents of values. * @param {Object} stack Tracks traversed `object` and `other` objects. * @returns {boolean} Returns `true` if the objects are equivalent, else `false`. */ function equalObjects(object, other, bitmask, customizer, equalFunc, stack) { var isPartial = bitmask & COMPARE_PARTIAL_FLAG, objProps = getAllKeys(object), objLength = objProps.length, othProps = getAllKeys(other), othLength = othProps.length; if (objLength != othLength && !isPartial) { return false; } var index = objLength; while (index--) { var key = objProps[index]; if (!(isPartial ? key in other : hasOwnProperty.call(other, key))) { return false; } } // Check that cyclic values are equal. var objStacked = stack.get(object); var othStacked = stack.get(other); if (objStacked && othStacked) { return objStacked == other && othStacked == object; } var result = true; stack.set(object, other); stack.set(other, object); var skipCtor = isPartial; while (++index < objLength) { key = objProps[index]; var objValue = object[key], othValue = other[key]; if (customizer) { var compared = isPartial ? customizer(othValue, objValue, key, other, object, stack) : customizer(objValue, othValue, key, object, other, stack); } // Recursively compare objects (susceptible to call stack limits). if (!(compared === undefined ? (objValue === othValue || equalFunc(objValue, othValue, bitmask, customizer, stack)) : compared )) { result = false; break; } skipCtor || (skipCtor = key == 'constructor'); } if (result && !skipCtor) { var objCtor = object.constructor, othCtor = other.constructor; // Non `Object` object instances with different constructors are not equal. if (objCtor != othCtor && ('constructor' in object && 'constructor' in other) && !(typeof objCtor == 'function' && objCtor instanceof objCtor && typeof othCtor == 'function' && othCtor instanceof othCtor)) { result = false; } } stack['delete'](object); stack['delete'](other); return result; } /** * A specialized version of `baseRest` which flattens the rest array. * * @private * @param {Function} func The function to apply a rest parameter to. * @returns {Function} Returns the new function. */ function flatRest(func) { return setToString(overRest(func, undefined, flatten), func + ''); } /** * Creates an array of own enumerable property names and symbols of `object`. * * @private * @param {Object} object The object to query. * @returns {Array} Returns the array of property names and symbols. */ function getAllKeys(object) { return baseGetAllKeys(object, keys, getSymbols); } /** * Creates an array of own and inherited enumerable property names and * symbols of `object`. * * @private * @param {Object} object The object to query. * @returns {Array} Returns the array of property names and symbols. */ function getAllKeysIn(object) { return baseGetAllKeys(object, keysIn, getSymbolsIn); } /** * Gets metadata for `func`. * * @private * @param {Function} func The function to query. * @returns {*} Returns the metadata for `func`. */ var getData = !metaMap ? noop : function(func) { return metaMap.get(func); }; /** * Gets the name of `func`. * * @private * @param {Function} func The function to query. * @returns {string} Returns the function name. */ function getFuncName(func) { var result = (func.name + ''), array = realNames[result], length = hasOwnProperty.call(realNames, result) ? array.length : 0; while (length--) { var data = array[length], otherFunc = data.func; if (otherFunc == null || otherFunc == func) { return data.name; } } return result; } /** * Gets the argument placeholder value for `func`. * * @private * @param {Function} func The function to inspect. * @returns {*} Returns the placeholder value. */ function getHolder(func) { var object = hasOwnProperty.call(lodash, 'placeholder') ? lodash : func; return object.placeholder; } /** * Gets the appropriate "iteratee" function. If `_.iteratee` is customized, * this function returns the custom method, otherwise it returns `baseIteratee`. * If arguments are provided, the chosen function is invoked with them and * its result is returned. * * @private * @param {*} [value] The value to convert to an iteratee. * @param {number} [arity] The arity of the created iteratee. * @returns {Function} Returns the chosen function or its result. */ function getIteratee() { var result = lodash.iteratee || iteratee; result = result === iteratee ? baseIteratee : result; return arguments.length ? result(arguments[0], arguments[1]) : result; } /** * Gets the data for `map`. * * @private * @param {Object} map The map to query. * @param {string} key The reference key. * @returns {*} Returns the map data. */ function getMapData(map, key) { var data = map.__data__; return isKeyable(key) ? data[typeof key == 'string' ? 'string' : 'hash'] : data.map; } /** * Gets the property names, values, and compare flags of `object`. * * @private * @param {Object} object The object to query. * @returns {Array} Returns the match data of `object`. */ function getMatchData(object) { var result = keys(object), length = result.length; while (length--) { var key = result[length], value = object[key]; result[length] = [key, value, isStrictComparable(value)]; } return result; } /** * Gets the native function at `key` of `object`. * * @private * @param {Object} object The object to query. * @param {string} key The key of the method to get. * @returns {*} Returns the function if it's native, else `undefined`. */ function getNative(object, key) { var value = getValue(object, key); return baseIsNative(value) ? value : undefined; } /** * A specialized version of `baseGetTag` which ignores `Symbol.toStringTag` values. * * @private * @param {*} value The value to query. * @returns {string} Returns the raw `toStringTag`. */ function getRawTag(value) { var isOwn = hasOwnProperty.call(value, symToStringTag), tag = value[symToStringTag]; try { value[symToStringTag] = undefined; var unmasked = true; } catch (e) {} var result = nativeObjectToString.call(value); if (unmasked) { if (isOwn) { value[symToStringTag] = tag; } else { delete value[symToStringTag]; } } return result; } /** * Creates an array of the own enumerable symbols of `object`. * * @private * @param {Object} object The object to query. * @returns {Array} Returns the array of symbols. */ var getSymbols = !nativeGetSymbols ? stubArray : function(object) { if (object == null) { return []; } object = Object(object); return arrayFilter(nativeGetSymbols(object), function(symbol) { return propertyIsEnumerable.call(object, symbol); }); }; /** * Creates an array of the own and inherited enumerable symbols of `object`. * * @private * @param {Object} object The object to query. * @returns {Array} Returns the array of symbols. */ var getSymbolsIn = !nativeGetSymbols ? stubArray : function(object) { var result = []; while (object) { arrayPush(result, getSymbols(object)); object = getPrototype(object); } return result; }; /** * Gets the `toStringTag` of `value`. * * @private * @param {*} value The value to query. * @returns {string} Returns the `toStringTag`. */ var getTag = baseGetTag; // Fallback for data views, maps, sets, and weak maps in IE 11 and promises in Node.js < 6. if ((DataView && getTag(new DataView(new ArrayBuffer(1))) != dataViewTag) || (Map && getTag(new Map) != mapTag) || (Promise && getTag(Promise.resolve()) != promiseTag) || (Set && getTag(new Set) != setTag) || (WeakMap && getTag(new WeakMap) != weakMapTag)) { getTag = function(value) { var result = baseGetTag(value), Ctor = result == objectTag ? value.constructor : undefined, ctorString = Ctor ? toSource(Ctor) : ''; if (ctorString) { switch (ctorString) { case dataViewCtorString: return dataViewTag; case mapCtorString: return mapTag; case promiseCtorString: return promiseTag; case setCtorString: return setTag; case weakMapCtorString: return weakMapTag; } } return result; }; } /** * Gets the view, applying any `transforms` to the `start` and `end` positions. * * @private * @param {number} start The start of the view. * @param {number} end The end of the view. * @param {Array} transforms The transformations to apply to the view. * @returns {Object} Returns an object containing the `start` and `end` * positions of the view. */ function getView(start, end, transforms) { var index = -1, length = transforms.length; while (++index < length) { var data = transforms[index], size = data.size; switch (data.type) { case 'drop': start += size; break; case 'dropRight': end -= size; break; case 'take': end = nativeMin(end, start + size); break; case 'takeRight': start = nativeMax(start, end - size); break; } } return { 'start': start, 'end': end }; } /** * Extracts wrapper details from the `source` body comment. * * @private * @param {string} source The source to inspect. * @returns {Array} Returns the wrapper details. */ function getWrapDetails(source) { var match = source.match(reWrapDetails); return match ? match[1].split(reSplitDetails) : []; } /** * Checks if `path` exists on `object`. * * @private * @param {Object} object The object to query. * @param {Array|string} path The path to check. * @param {Function} hasFunc The function to check properties. * @returns {boolean} Returns `true` if `path` exists, else `false`. */ function hasPath(object, path, hasFunc) { path = castPath(path, object); var index = -1, length = path.length, result = false; while (++index < length) { var key = toKey(path[index]); if (!(result = object != null && hasFunc(object, key))) { break; } object = object[key]; } if (result || ++index != length) { return result; } length = object == null ? 0 : object.length; return !!length && isLength(length) && isIndex(key, length) && (isArray(object) || isArguments(object)); } /** * Initializes an array clone. * * @private * @param {Array} array The array to clone. * @returns {Array} Returns the initialized clone. */ function initCloneArray(array) { var length = array.length, result = new array.constructor(length); // Add properties assigned by `RegExp#exec`. if (length && typeof array[0] == 'string' && hasOwnProperty.call(array, 'index')) { result.index = array.index; result.input = array.input; } return result; } /** * Initializes an object clone. * * @private * @param {Object} object The object to clone. * @returns {Object} Returns the initialized clone. */ function initCloneObject(object) { return (typeof object.constructor == 'function' && !isPrototype(object)) ? baseCreate(getPrototype(object)) : {}; } /** * Initializes an object clone based on its `toStringTag`. * * **Note:** This function only supports cloning values with tags of * `Boolean`, `Date`, `Error`, `Map`, `Number`, `RegExp`, `Set`, or `String`. * * @private * @param {Object} object The object to clone. * @param {string} tag The `toStringTag` of the object to clone. * @param {boolean} [isDeep] Specify a deep clone. * @returns {Object} Returns the initialized clone. */ function initCloneByTag(object, tag, isDeep) { var Ctor = object.constructor; switch (tag) { case arrayBufferTag: return cloneArrayBuffer(object); case boolTag: case dateTag: return new Ctor(+object); case dataViewTag: return cloneDataView(object, isDeep); case float32Tag: case float64Tag: case int8Tag: case int16Tag: case int32Tag: case uint8Tag: case uint8ClampedTag: case uint16Tag: case uint32Tag: return cloneTypedArray(object, isDeep); case mapTag: return new Ctor; case numberTag: case stringTag: return new Ctor(object); case regexpTag: return cloneRegExp(object); case setTag: return new Ctor; case symbolTag: return cloneSymbol(object); } } /** * Inserts wrapper `details` in a comment at the top of the `source` body. * * @private * @param {string} source The source to modify. * @returns {Array} details The details to insert. * @returns {string} Returns the modified source. */ function insertWrapDetails(source, details) { var length = details.length; if (!length) { return source; } var lastIndex = length - 1; details[lastIndex] = (length > 1 ? '& ' : '') + details[lastIndex]; details = details.join(length > 2 ? ', ' : ' '); return source.replace(reWrapComment, '{\n/* [wrapped with ' + details + '] */\n'); } /** * Checks if `value` is a flattenable `arguments` object or array. * * @private * @param {*} value The value to check. * @returns {boolean} Returns `true` if `value` is flattenable, else `false`. */ function isFlattenable(value) { return isArray(value) || isArguments(value) || !!(spreadableSymbol && value && value[spreadableSymbol]); } /** * Checks if `value` is a valid array-like index. * * @private * @param {*} value The value to check. * @param {number} [length=MAX_SAFE_INTEGER] The upper bounds of a valid index. * @returns {boolean} Returns `true` if `value` is a valid index, else `false`. */ function isIndex(value, length) { var type = typeof value; length = length == null ? MAX_SAFE_INTEGER : length; return !!length && (type == 'number' || (type != 'symbol' && reIsUint.test(value))) && (value > -1 && value % 1 == 0 && value < length); } /** * Checks if the given arguments are from an iteratee call. * * @private * @param {*} value The potential iteratee value argument. * @param {*} index The potential iteratee index or key argument. * @param {*} object The potential iteratee object argument. * @returns {boolean} Returns `true` if the arguments are from an iteratee call, * else `false`. */ function isIterateeCall(value, index, object) { if (!isObject(object)) { return false; } var type = typeof index; if (type == 'number' ? (isArrayLike(object) && isIndex(index, object.length)) : (type == 'string' && index in object) ) { return eq(object[index], value); } return false; } /** * Checks if `value` is a property name and not a property path. * * @private * @param {*} value The value to check. * @param {Object} [object] The object to query keys on. * @returns {boolean} Returns `true` if `value` is a property name, else `false`. */ function isKey(value, object) { if (isArray(value)) { return false; } var type = typeof value; if (type == 'number' || type == 'symbol' || type == 'boolean' || value == null || isSymbol(value)) { return true; } return reIsPlainProp.test(value) || !reIsDeepProp.test(value) || (object != null && value in Object(object)); } /** * Checks if `value` is suitable for use as unique object key. * * @private * @param {*} value The value to check. * @returns {boolean} Returns `true` if `value` is suitable, else `false`. */ function isKeyable(value) { var type = typeof value; return (type == 'string' || type == 'number' || type == 'symbol' || type == 'boolean') ? (value !== '__proto__') : (value === null); } /** * Checks if `func` has a lazy counterpart. * * @private * @param {Function} func The function to check. * @returns {boolean} Returns `true` if `func` has a lazy counterpart, * else `false`. */ function isLaziable(func) { var funcName = getFuncName(func), other = lodash[funcName]; if (typeof other != 'function' || !(funcName in LazyWrapper.prototype)) { return false; } if (func === other) { return true; } var data = getData(other); return !!data && func === data[0]; } /** * Checks if `func` has its source masked. * * @private * @param {Function} func The function to check. * @returns {boolean} Returns `true` if `func` is masked, else `false`. */ function isMasked(func) { return !!maskSrcKey && (maskSrcKey in func); } /** * Checks if `func` is capable of being masked. * * @private * @param {*} value The value to check. * @returns {boolean} Returns `true` if `func` is maskable, else `false`. */ var isMaskable = coreJsData ? isFunction : stubFalse; /** * Checks if `value` is likely a prototype object. * * @private * @param {*} value The value to check. * @returns {boolean} Returns `true` if `value` is a prototype, else `false`. */ function isPrototype(value) { var Ctor = value && value.constructor, proto = (typeof Ctor == 'function' && Ctor.prototype) || objectProto; return value === proto; } /** * Checks if `value` is suitable for strict equality comparisons, i.e. `===`. * * @private * @param {*} value The value to check. * @returns {boolean} Returns `true` if `value` if suitable for strict * equality comparisons, else `false`. */ function isStrictComparable(value) { return value === value && !isObject(value); } /** * A specialized version of `matchesProperty` for source values suitable * for strict equality comparisons, i.e. `===`. * * @private * @param {string} key The key of the property to get. * @param {*} srcValue The value to match. * @returns {Function} Returns the new spec function. */ function matchesStrictComparable(key, srcValue) { return function(object) { if (object == null) { return false; } return object[key] === srcValue && (srcValue !== undefined || (key in Object(object))); }; } /** * A specialized version of `_.memoize` which clears the memoized function's * cache when it exceeds `MAX_MEMOIZE_SIZE`. * * @private * @param {Function} func The function to have its output memoized. * @returns {Function} Returns the new memoized function. */ function memoizeCapped(func) { var result = memoize(func, function(key) { if (cache.size === MAX_MEMOIZE_SIZE) { cache.clear(); } return key; }); var cache = result.cache; return result; } /** * Merges the function metadata of `source` into `data`. * * Merging metadata reduces the number of wrappers used to invoke a function. * This is possible because methods like `_.bind`, `_.curry`, and `_.partial` * may be applied regardless of execution order. Methods like `_.ary` and * `_.rearg` modify function arguments, making the order in which they are * executed important, preventing the merging of metadata. However, we make * an exception for a safe combined case where curried functions have `_.ary` * and or `_.rearg` applied. * * @private * @param {Array} data The destination metadata. * @param {Array} source The source metadata. * @returns {Array} Returns `data`. */ function mergeData(data, source) { var bitmask = data[1], srcBitmask = source[1], newBitmask = bitmask | srcBitmask, isCommon = newBitmask < (WRAP_BIND_FLAG | WRAP_BIND_KEY_FLAG | WRAP_ARY_FLAG); var isCombo = ((srcBitmask == WRAP_ARY_FLAG) && (bitmask == WRAP_CURRY_FLAG)) || ((srcBitmask == WRAP_ARY_FLAG) && (bitmask == WRAP_REARG_FLAG) && (data[7].length <= source[8])) || ((srcBitmask == (WRAP_ARY_FLAG | WRAP_REARG_FLAG)) && (source[7].length <= source[8]) && (bitmask == WRAP_CURRY_FLAG)); // Exit early if metadata can't be merged. if (!(isCommon || isCombo)) { return data; } // Use source `thisArg` if available. if (srcBitmask & WRAP_BIND_FLAG) { data[2] = source[2]; // Set when currying a bound function. newBitmask |= bitmask & WRAP_BIND_FLAG ? 0 : WRAP_CURRY_BOUND_FLAG; } // Compose partial arguments. var value = source[3]; if (value) { var partials = data[3]; data[3] = partials ? composeArgs(partials, value, source[4]) : value; data[4] = partials ? replaceHolders(data[3], PLACEHOLDER) : source[4]; } // Compose partial right arguments. value = source[5]; if (value) { partials = data[5]; data[5] = partials ? composeArgsRight(partials, value, source[6]) : value; data[6] = partials ? replaceHolders(data[5], PLACEHOLDER) : source[6]; } // Use source `argPos` if available. value = source[7]; if (value) { data[7] = value; } // Use source `ary` if it's smaller. if (srcBitmask & WRAP_ARY_FLAG) { data[8] = data[8] == null ? source[8] : nativeMin(data[8], source[8]); } // Use source `arity` if one is not provided. if (data[9] == null) { data[9] = source[9]; } // Use source `func` and merge bitmasks. data[0] = source[0]; data[1] = newBitmask; return data; } /** * This function is like * [`Object.keys`](http://ecma-international.org/ecma-262/7.0/#sec-object.keys) * except that it includes inherited enumerable properties. * * @private * @param {Object} object The object to query. * @returns {Array} Returns the array of property names. */ function nativeKeysIn(object) { var result = []; if (object != null) { for (var key in Object(object)) { result.push(key); } } return result; } /** * Converts `value` to a string using `Object.prototype.toString`. * * @private * @param {*} value The value to convert. * @returns {string} Returns the converted string. */ function objectToString(value) { return nativeObjectToString.call(value); } /** * A specialized version of `baseRest` which transforms the rest array. * * @private * @param {Function} func The function to apply a rest parameter to. * @param {number} [start=func.length-1] The start position of the rest parameter. * @param {Function} transform The rest array transform. * @returns {Function} Returns the new function. */ function overRest(func, start, transform) { start = nativeMax(start === undefined ? (func.length - 1) : start, 0); return function() { var args = arguments, index = -1, length = nativeMax(args.length - start, 0), array = Array(length); while (++index < length) { array[index] = args[start + index]; } index = -1; var otherArgs = Array(start + 1); while (++index < start) { otherArgs[index] = args[index]; } otherArgs[start] = transform(array); return apply(func, this, otherArgs); }; } /** * Gets the parent value at `path` of `object`. * * @private * @param {Object} object The object to query. * @param {Array} path The path to get the parent value of. * @returns {*} Returns the parent value. */ function parent(object, path) { return path.length < 2 ? object : baseGet(object, baseSlice(path, 0, -1)); } /** * Reorder `array` according to the specified indexes where the element at * the first index is assigned as the first element, the element at * the second index is assigned as the second element, and so on. * * @private * @param {Array} array The array to reorder. * @param {Array} indexes The arranged array indexes. * @returns {Array} Returns `array`. */ function reorder(array, indexes) { var arrLength = array.length, length = nativeMin(indexes.length, arrLength), oldArray = copyArray(array); while (length--) { var index = indexes[length]; array[length] = isIndex(index, arrLength) ? oldArray[index] : undefined; } return array; } /** * Gets the value at `key`, unless `key` is "__proto__" or "constructor". * * @private * @param {Object} object The object to query. * @param {string} key The key of the property to get. * @returns {*} Returns the property value. */ function safeGet(object, key) { if (key === 'constructor' && typeof object[key] === 'function') { return; } if (key == '__proto__') { return; } return object[key]; } /** * Sets metadata for `func`. * * **Note:** If this function becomes hot, i.e. is invoked a lot in a short * period of time, it will trip its breaker and transition to an identity * function to avoid garbage collection pauses in V8. See * [V8 issue 2070](https://bugs.chromium.org/p/v8/issues/detail?id=2070) * for more details. * * @private * @param {Function} func The function to associate metadata with. * @param {*} data The metadata. * @returns {Function} Returns `func`. */ var setData = shortOut(baseSetData); /** * A simple wrapper around the global [`setTimeout`](https://mdn.io/setTimeout). * * @private * @param {Function} func The function to delay. * @param {number} wait The number of milliseconds to delay invocation. * @returns {number|Object} Returns the timer id or timeout object. */ var setTimeout = ctxSetTimeout || function(func, wait) { return root.setTimeout(func, wait); }; /** * Sets the `toString` method of `func` to return `string`. * * @private * @param {Function} func The function to modify. * @param {Function} string The `toString` result. * @returns {Function} Returns `func`. */ var setToString = shortOut(baseSetToString); /** * Sets the `toString` method of `wrapper` to mimic the source of `reference` * with wrapper details in a comment at the top of the source body. * * @private * @param {Function} wrapper The function to modify. * @param {Function} reference The reference function. * @param {number} bitmask The bitmask flags. See `createWrap` for more details. * @returns {Function} Returns `wrapper`. */ function setWrapToString(wrapper, reference, bitmask) { var source = (reference + ''); return setToString(wrapper, insertWrapDetails(source, updateWrapDetails(getWrapDetails(source), bitmask))); } /** * Creates a function that'll short out and invoke `identity` instead * of `func` when it's called `HOT_COUNT` or more times in `HOT_SPAN` * milliseconds. * * @private * @param {Function} func The function to restrict. * @returns {Function} Returns the new shortable function. */ function shortOut(func) { var count = 0, lastCalled = 0; return function() { var stamp = nativeNow(), remaining = HOT_SPAN - (stamp - lastCalled); lastCalled = stamp; if (remaining > 0) { if (++count >= HOT_COUNT) { return arguments[0]; } } else { count = 0; } return func.apply(undefined, arguments); }; } /** * A specialized version of `_.shuffle` which mutates and sets the size of `array`. * * @private * @param {Array} array The array to shuffle. * @param {number} [size=array.length] The size of `array`. * @returns {Array} Returns `array`. */ function shuffleSelf(array, size) { var index = -1, length = array.length, lastIndex = length - 1; size = size === undefined ? length : size; while (++index < size) { var rand = baseRandom(index, lastIndex), value = array[rand]; array[rand] = array[index]; array[index] = value; } array.length = size; return array; } /** * Converts `string` to a property path array. * * @private * @param {string} string The string to convert. * @returns {Array} Returns the property path array. */ var stringToPath = memoizeCapped(function(string) { var result = []; if (string.charCodeAt(0) === 46 /* . */) { result.push(''); } string.replace(rePropName, function(match, number, quote, subString) { result.push(quote ? subString.replace(reEscapeChar, '$1') : (number || match)); }); return result; }); /** * Converts `value` to a string key if it's not a string or symbol. * * @private * @param {*} value The value to inspect. * @returns {string|symbol} Returns the key. */ function toKey(value) { if (typeof value == 'string' || isSymbol(value)) { return value; } var result = (value + ''); return (result == '0' && (1 / value) == -INFINITY) ? '-0' : result; } /** * Converts `func` to its source code. * * @private * @param {Function} func The function to convert. * @returns {string} Returns the source code. */ function toSource(func) { if (func != null) { try { return funcToString.call(func); } catch (e) {} try { return (func + ''); } catch (e) {} } return ''; } /** * Updates wrapper `details` based on `bitmask` flags. * * @private * @returns {Array} details The details to modify. * @param {number} bitmask The bitmask flags. See `createWrap` for more details. * @returns {Array} Returns `details`. */ function updateWrapDetails(details, bitmask) { arrayEach(wrapFlags, function(pair) { var value = '_.' + pair[0]; if ((bitmask & pair[1]) && !arrayIncludes(details, value)) { details.push(value); } }); return details.sort(); } /** * Creates a clone of `wrapper`. * * @private * @param {Object} wrapper The wrapper to clone. * @returns {Object} Returns the cloned wrapper. */ function wrapperClone(wrapper) { if (wrapper instanceof LazyWrapper) { return wrapper.clone(); } var result = new LodashWrapper(wrapper.__wrapped__, wrapper.__chain__); result.__actions__ = copyArray(wrapper.__actions__); result.__index__ = wrapper.__index__; result.__values__ = wrapper.__values__; return result; } /*------------------------------------------------------------------------*/ /** * Creates an array of elements split into groups the length of `size`. * If `array` can't be split evenly, the final chunk will be the remaining * elements. * * @static * @memberOf _ * @since 3.0.0 * @category Array * @param {Array} array The array to process. * @param {number} [size=1] The length of each chunk * @param- {Object} [guard] Enables use as an iteratee for methods like `_.map`. * @returns {Array} Returns the new array of chunks. * @example * * _.chunk(['a', 'b', 'c', 'd'], 2); * // => [['a', 'b'], ['c', 'd']] * * _.chunk(['a', 'b', 'c', 'd'], 3); * // => [['a', 'b', 'c'], ['d']] */ function chunk(array, size, guard) { if ((guard ? isIterateeCall(array, size, guard) : size === undefined)) { size = 1; } else { size = nativeMax(toInteger(size), 0); } var length = array == null ? 0 : array.length; if (!length || size < 1) { return []; } var index = 0, resIndex = 0, result = Array(nativeCeil(length / size)); while (index < length) { result[resIndex++] = baseSlice(array, index, (index += size)); } return result; } /** * Creates an array with all falsey values removed. The values `false`, `null`, * `0`, `""`, `undefined`, and `NaN` are falsey. * * @static * @memberOf _ * @since 0.1.0 * @category Array * @param {Array} array The array to compact. * @returns {Array} Returns the new array of filtered values. * @example * * _.compact([0, 1, false, 2, '', 3]); * // => [1, 2, 3] */ function compact(array) { var index = -1, length = array == null ? 0 : array.length, resIndex = 0, result = []; while (++index < length) { var value = array[index]; if (value) { result[resIndex++] = value; } } return result; } /** * Creates a new array concatenating `array` with any additional arrays * and/or values. * * @static * @memberOf _ * @since 4.0.0 * @category Array * @param {Array} array The array to concatenate. * @param {...*} [values] The values to concatenate. * @returns {Array} Returns the new concatenated array. * @example * * var array = [1]; * var other = _.concat(array, 2, [3], [[4]]); * * console.log(other); * // => [1, 2, 3, [4]] * * console.log(array); * // => [1] */ function concat() { var length = arguments.length; if (!length) { return []; } var args = Array(length - 1), array = arguments[0], index = length; while (index--) { args[index - 1] = arguments[index]; } return arrayPush(isArray(array) ? copyArray(array) : [array], baseFlatten(args, 1)); } /** * Creates an array of `array` values not included in the other given arrays * using [`SameValueZero`](http://ecma-international.org/ecma-262/7.0/#sec-samevaluezero) * for equality comparisons. The order and references of result values are * determined by the first array. * * **Note:** Unlike `_.pullAll`, this method returns a new array. * * @static * @memberOf _ * @since 0.1.0 * @category Array * @param {Array} array The array to inspect. * @param {...Array} [values] The values to exclude. * @returns {Array} Returns the new array of filtered values. * @see _.without, _.xor * @example * * _.difference([2, 1], [2, 3]); * // => [1] */ var difference = baseRest(function(array, values) { return isArrayLikeObject(array) ? baseDifference(array, baseFlatten(values, 1, isArrayLikeObject, true)) : []; }); /** * This method is like `_.difference` except that it accepts `iteratee` which * is invoked for each element of `array` and `values` to generate the criterion * by which they're compared. The order and references of result values are * determined by the first array. The iteratee is invoked with one argument: * (value). * * **Note:** Unlike `_.pullAllBy`, this method returns a new array. * * @static * @memberOf _ * @since 4.0.0 * @category Array * @param {Array} array The array to inspect. * @param {...Array} [values] The values to exclude. * @param {Function} [iteratee=_.identity] The iteratee invoked per element. * @returns {Array} Returns the new array of filtered values. * @example * * _.differenceBy([2.1, 1.2], [2.3, 3.4], Math.floor); * // => [1.2] * * // The `_.property` iteratee shorthand. * _.differenceBy([{ 'x': 2 }, { 'x': 1 }], [{ 'x': 1 }], 'x'); * // => [{ 'x': 2 }] */ var differenceBy = baseRest(function(array, values) { var iteratee = last(values); if (isArrayLikeObject(iteratee)) { iteratee = undefined; } return isArrayLikeObject(array) ? baseDifference(array, baseFlatten(values, 1, isArrayLikeObject, true), getIteratee(iteratee, 2)) : []; }); /** * This method is like `_.difference` except that it accepts `comparator` * which is invoked to compare elements of `array` to `values`. The order and * references of result values are determined by the first array. The comparator * is invoked with two arguments: (arrVal, othVal). * * **Note:** Unlike `_.pullAllWith`, this method returns a new array. * * @static * @memberOf _ * @since 4.0.0 * @category Array * @param {Array} array The array to inspect. * @param {...Array} [values] The values to exclude. * @param {Function} [comparator] The comparator invoked per element. * @returns {Array} Returns the new array of filtered values. * @example * * var objects = [{ 'x': 1, 'y': 2 }, { 'x': 2, 'y': 1 }]; * * _.differenceWith(objects, [{ 'x': 1, 'y': 2 }], _.isEqual); * // => [{ 'x': 2, 'y': 1 }] */ var differenceWith = baseRest(function(array, values) { var comparator = last(values); if (isArrayLikeObject(comparator)) { comparator = undefined; } return isArrayLikeObject(array) ? baseDifference(array, baseFlatten(values, 1, isArrayLikeObject, true), undefined, comparator) : []; }); /** * Creates a slice of `array` with `n` elements dropped from the beginning. * * @static * @memberOf _ * @since 0.5.0 * @category Array * @param {Array} array The array to query. * @param {number} [n=1] The number of elements to drop. * @param- {Object} [guard] Enables use as an iteratee for methods like `_.map`. * @returns {Array} Returns the slice of `array`. * @example * * _.drop([1, 2, 3]); * // => [2, 3] * * _.drop([1, 2, 3], 2); * // => [3] * * _.drop([1, 2, 3], 5); * // => [] * * _.drop([1, 2, 3], 0); * // => [1, 2, 3] */ function drop(array, n, guard) { var length = array == null ? 0 : array.length; if (!length) { return []; } n = (guard || n === undefined) ? 1 : toInteger(n); return baseSlice(array, n < 0 ? 0 : n, length); } /** * Creates a slice of `array` with `n` elements dropped from the end. * * @static * @memberOf _ * @since 3.0.0 * @category Array * @param {Array} array The array to query. * @param {number} [n=1] The number of elements to drop. * @param- {Object} [guard] Enables use as an iteratee for methods like `_.map`. * @returns {Array} Returns the slice of `array`. * @example * * _.dropRight([1, 2, 3]); * // => [1, 2] * * _.dropRight([1, 2, 3], 2); * // => [1] * * _.dropRight([1, 2, 3], 5); * // => [] * * _.dropRight([1, 2, 3], 0); * // => [1, 2, 3] */ function dropRight(array, n, guard) { var length = array == null ? 0 : array.length; if (!length) { return []; } n = (guard || n === undefined) ? 1 : toInteger(n); n = length - n; return baseSlice(array, 0, n < 0 ? 0 : n); } /** * Creates a slice of `array` excluding elements dropped from the end. * Elements are dropped until `predicate` returns falsey. The predicate is * invoked with three arguments: (value, index, array). * * @static * @memberOf _ * @since 3.0.0 * @category Array * @param {Array} array The array to query. * @param {Function} [predicate=_.identity] The function invoked per iteration. * @returns {Array} Returns the slice of `array`. * @example * * var users = [ * { 'user': 'barney', 'active': true }, * { 'user': 'fred', 'active': false }, * { 'user': 'pebbles', 'active': false } * ]; * * _.dropRightWhile(users, function(o) { return !o.active; }); * // => objects for ['barney'] * * // The `_.matches` iteratee shorthand. * _.dropRightWhile(users, { 'user': 'pebbles', 'active': false }); * // => objects for ['barney', 'fred'] * * // The `_.matchesProperty` iteratee shorthand. * _.dropRightWhile(users, ['active', false]); * // => objects for ['barney'] * * // The `_.property` iteratee shorthand. * _.dropRightWhile(users, 'active'); * // => objects for ['barney', 'fred', 'pebbles'] */ function dropRightWhile(array, predicate) { return (array && array.length) ? baseWhile(array, getIteratee(predicate, 3), true, true) : []; } /** * Creates a slice of `array` excluding elements dropped from the beginning. * Elements are dropped until `predicate` returns falsey. The predicate is * invoked with three arguments: (value, index, array). * * @static * @memberOf _ * @since 3.0.0 * @category Array * @param {Array} array The array to query. * @param {Function} [predicate=_.identity] The function invoked per iteration. * @returns {Array} Returns the slice of `array`. * @example * * var users = [ * { 'user': 'barney', 'active': false }, * { 'user': 'fred', 'active': false }, * { 'user': 'pebbles', 'active': true } * ]; * * _.dropWhile(users, function(o) { return !o.active; }); * // => objects for ['pebbles'] * * // The `_.matches` iteratee shorthand. * _.dropWhile(users, { 'user': 'barney', 'active': false }); * // => objects for ['fred', 'pebbles'] * * // The `_.matchesProperty` iteratee shorthand. * _.dropWhile(users, ['active', false]); * // => objects for ['pebbles'] * * // The `_.property` iteratee shorthand. * _.dropWhile(users, 'active'); * // => objects for ['barney', 'fred', 'pebbles'] */ function dropWhile(array, predicate) { return (array && array.length) ? baseWhile(array, getIteratee(predicate, 3), true) : []; } /** * Fills elements of `array` with `value` from `start` up to, but not * including, `end`. * * **Note:** This method mutates `array`. * * @static * @memberOf _ * @since 3.2.0 * @category Array * @param {Array} array The array to fill. * @param {*} value The value to fill `array` with. * @param {number} [start=0] The start position. * @param {number} [end=array.length] The end position. * @returns {Array} Returns `array`. * @example * * var array = [1, 2, 3]; * * _.fill(array, 'a'); * console.log(array); * // => ['a', 'a', 'a'] * * _.fill(Array(3), 2); * // => [2, 2, 2] * * _.fill([4, 6, 8, 10], '*', 1, 3); * // => [4, '*', '*', 10] */ function fill(array, value, start, end) { var length = array == null ? 0 : array.length; if (!length) { return []; } if (start && typeof start != 'number' && isIterateeCall(array, value, start)) { start = 0; end = length; } return baseFill(array, value, start, end); } /** * This method is like `_.find` except that it returns the index of the first * element `predicate` returns truthy for instead of the element itself. * * @static * @memberOf _ * @since 1.1.0 * @category Array * @param {Array} array The array to inspect. * @param {Function} [predicate=_.identity] The function invoked per iteration. * @param {number} [fromIndex=0] The index to search from. * @returns {number} Returns the index of the found element, else `-1`. * @example * * var users = [ * { 'user': 'barney', 'active': false }, * { 'user': 'fred', 'active': false }, * { 'user': 'pebbles', 'active': true } * ]; * * _.findIndex(users, function(o) { return o.user == 'barney'; }); * // => 0 * * // The `_.matches` iteratee shorthand. * _.findIndex(users, { 'user': 'fred', 'active': false }); * // => 1 * * // The `_.matchesProperty` iteratee shorthand. * _.findIndex(users, ['active', false]); * // => 0 * * // The `_.property` iteratee shorthand. * _.findIndex(users, 'active'); * // => 2 */ function findIndex(array, predicate, fromIndex) { var length = array == null ? 0 : array.length; if (!length) { return -1; } var index = fromIndex == null ? 0 : toInteger(fromIndex); if (index < 0) { index = nativeMax(length + index, 0); } return baseFindIndex(array, getIteratee(predicate, 3), index); } /** * This method is like `_.findIndex` except that it iterates over elements * of `collection` from right to left. * * @static * @memberOf _ * @since 2.0.0 * @category Array * @param {Array} array The array to inspect. * @param {Function} [predicate=_.identity] The function invoked per iteration. * @param {number} [fromIndex=array.length-1] The index to search from. * @returns {number} Returns the index of the found element, else `-1`. * @example * * var users = [ * { 'user': 'barney', 'active': true }, * { 'user': 'fred', 'active': false }, * { 'user': 'pebbles', 'active': false } * ]; * * _.findLastIndex(users, function(o) { return o.user == 'pebbles'; }); * // => 2 * * // The `_.matches` iteratee shorthand. * _.findLastIndex(users, { 'user': 'barney', 'active': true }); * // => 0 * * // The `_.matchesProperty` iteratee shorthand. * _.findLastIndex(users, ['active', false]); * // => 2 * * // The `_.property` iteratee shorthand. * _.findLastIndex(users, 'active'); * // => 0 */ function findLastIndex(array, predicate, fromIndex) { var length = array == null ? 0 : array.length; if (!length) { return -1; } var index = length - 1; if (fromIndex !== undefined) { index = toInteger(fromIndex); index = fromIndex < 0 ? nativeMax(length + index, 0) : nativeMin(index, length - 1); } return baseFindIndex(array, getIteratee(predicate, 3), index, true); } /** * Flattens `array` a single level deep. * * @static * @memberOf _ * @since 0.1.0 * @category Array * @param {Array} array The array to flatten. * @returns {Array} Returns the new flattened array. * @example * * _.flatten([1, [2, [3, [4]], 5]]); * // => [1, 2, [3, [4]], 5] */ function flatten(array) { var length = array == null ? 0 : array.length; return length ? baseFlatten(array, 1) : []; } /** * Recursively flattens `array`. * * @static * @memberOf _ * @since 3.0.0 * @category Array * @param {Array} array The array to flatten. * @returns {Array} Returns the new flattened array. * @example * * _.flattenDeep([1, [2, [3, [4]], 5]]); * // => [1, 2, 3, 4, 5] */ function flattenDeep(array) { var length = array == null ? 0 : array.length; return length ? baseFlatten(array, INFINITY) : []; } /** * Recursively flatten `array` up to `depth` times. * * @static * @memberOf _ * @since 4.4.0 * @category Array * @param {Array} array The array to flatten. * @param {number} [depth=1] The maximum recursion depth. * @returns {Array} Returns the new flattened array. * @example * * var array = [1, [2, [3, [4]], 5]]; * * _.flattenDepth(array, 1); * // => [1, 2, [3, [4]], 5] * * _.flattenDepth(array, 2); * // => [1, 2, 3, [4], 5] */ function flattenDepth(array, depth) { var length = array == null ? 0 : array.length; if (!length) { return []; } depth = depth === undefined ? 1 : toInteger(depth); return baseFlatten(array, depth); } /** * The inverse of `_.toPairs`; this method returns an object composed * from key-value `pairs`. * * @static * @memberOf _ * @since 4.0.0 * @category Array * @param {Array} pairs The key-value pairs. * @returns {Object} Returns the new object. * @example * * _.fromPairs([['a', 1], ['b', 2]]); * // => { 'a': 1, 'b': 2 } */ function fromPairs(pairs) { var index = -1, length = pairs == null ? 0 : pairs.length, result = {}; while (++index < length) { var pair = pairs[index]; result[pair[0]] = pair[1]; } return result; } /** * Gets the first element of `array`. * * @static * @memberOf _ * @since 0.1.0 * @alias first * @category Array * @param {Array} array The array to query. * @returns {*} Returns the first element of `array`. * @example * * _.head([1, 2, 3]); * // => 1 * * _.head([]); * // => undefined */ function head(array) { return (array && array.length) ? array[0] : undefined; } /** * Gets the index at which the first occurrence of `value` is found in `array` * using [`SameValueZero`](http://ecma-international.org/ecma-262/7.0/#sec-samevaluezero) * for equality comparisons. If `fromIndex` is negative, it's used as the * offset from the end of `array`. * * @static * @memberOf _ * @since 0.1.0 * @category Array * @param {Array} array The array to inspect. * @param {*} value The value to search for. * @param {number} [fromIndex=0] The index to search from. * @returns {number} Returns the index of the matched value, else `-1`. * @example * * _.indexOf([1, 2, 1, 2], 2); * // => 1 * * // Search from the `fromIndex`. * _.indexOf([1, 2, 1, 2], 2, 2); * // => 3 */ function indexOf(array, value, fromIndex) { var length = array == null ? 0 : array.length; if (!length) { return -1; } var index = fromIndex == null ? 0 : toInteger(fromIndex); if (index < 0) { index = nativeMax(length + index, 0); } return baseIndexOf(array, value, index); } /** * Gets all but the last element of `array`. * * @static * @memberOf _ * @since 0.1.0 * @category Array * @param {Array} array The array to query. * @returns {Array} Returns the slice of `array`. * @example * * _.initial([1, 2, 3]); * // => [1, 2] */ function initial(array) { var length = array == null ? 0 : array.length; return length ? baseSlice(array, 0, -1) : []; } /** * Creates an array of unique values that are included in all given arrays * using [`SameValueZero`](http://ecma-international.org/ecma-262/7.0/#sec-samevaluezero) * for equality comparisons. The order and references of result values are * determined by the first array. * * @static * @memberOf _ * @since 0.1.0 * @category Array * @param {...Array} [arrays] The arrays to inspect. * @returns {Array} Returns the new array of intersecting values. * @example * * _.intersection([2, 1], [2, 3]); * // => [2] */ var intersection = baseRest(function(arrays) { var mapped = arrayMap(arrays, castArrayLikeObject); return (mapped.length && mapped[0] === arrays[0]) ? baseIntersection(mapped) : []; }); /** * This method is like `_.intersection` except that it accepts `iteratee` * which is invoked for each element of each `arrays` to generate the criterion * by which they're compared. The order and references of result values are * determined by the first array. The iteratee is invoked with one argument: * (value). * * @static * @memberOf _ * @since 4.0.0 * @category Array * @param {...Array} [arrays] The arrays to inspect. * @param {Function} [iteratee=_.identity] The iteratee invoked per element. * @returns {Array} Returns the new array of intersecting values. * @example * * _.intersectionBy([2.1, 1.2], [2.3, 3.4], Math.floor); * // => [2.1] * * // The `_.property` iteratee shorthand. * _.intersectionBy([{ 'x': 1 }], [{ 'x': 2 }, { 'x': 1 }], 'x'); * // => [{ 'x': 1 }] */ var intersectionBy = baseRest(function(arrays) { var iteratee = last(arrays), mapped = arrayMap(arrays, castArrayLikeObject); if (iteratee === last(mapped)) { iteratee = undefined; } else { mapped.pop(); } return (mapped.length && mapped[0] === arrays[0]) ? baseIntersection(mapped, getIteratee(iteratee, 2)) : []; }); /** * This method is like `_.intersection` except that it accepts `comparator` * which is invoked to compare elements of `arrays`. The order and references * of result values are determined by the first array. The comparator is * invoked with two arguments: (arrVal, othVal). * * @static * @memberOf _ * @since 4.0.0 * @category Array * @param {...Array} [arrays] The arrays to inspect. * @param {Function} [comparator] The comparator invoked per element. * @returns {Array} Returns the new array of intersecting values. * @example * * var objects = [{ 'x': 1, 'y': 2 }, { 'x': 2, 'y': 1 }]; * var others = [{ 'x': 1, 'y': 1 }, { 'x': 1, 'y': 2 }]; * * _.intersectionWith(objects, others, _.isEqual); * // => [{ 'x': 1, 'y': 2 }] */ var intersectionWith = baseRest(function(arrays) { var comparator = last(arrays), mapped = arrayMap(arrays, castArrayLikeObject); comparator = typeof comparator == 'function' ? comparator : undefined; if (comparator) { mapped.pop(); } return (mapped.length && mapped[0] === arrays[0]) ? baseIntersection(mapped, undefined, comparator) : []; }); /** * Converts all elements in `array` into a string separated by `separator`. * * @static * @memberOf _ * @since 4.0.0 * @category Array * @param {Array} array The array to convert. * @param {string} [separator=','] The element separator. * @returns {string} Returns the joined string. * @example * * _.join(['a', 'b', 'c'], '~'); * // => 'a~b~c' */ function join(array, separator) { return array == null ? '' : nativeJoin.call(array, separator); } /** * Gets the last element of `array`. * * @static * @memberOf _ * @since 0.1.0 * @category Array * @param {Array} array The array to query. * @returns {*} Returns the last element of `array`. * @example * * _.last([1, 2, 3]); * // => 3 */ function last(array) { var length = array == null ? 0 : array.length; return length ? array[length - 1] : undefined; } /** * This method is like `_.indexOf` except that it iterates over elements of * `array` from right to left. * * @static * @memberOf _ * @since 0.1.0 * @category Array * @param {Array} array The array to inspect. * @param {*} value The value to search for. * @param {number} [fromIndex=array.length-1] The index to search from. * @returns {number} Returns the index of the matched value, else `-1`. * @example * * _.lastIndexOf([1, 2, 1, 2], 2); * // => 3 * * // Search from the `fromIndex`. * _.lastIndexOf([1, 2, 1, 2], 2, 2); * // => 1 */ function lastIndexOf(array, value, fromIndex) { var length = array == null ? 0 : array.length; if (!length) { return -1; } var index = length; if (fromIndex !== undefined) { index = toInteger(fromIndex); index = index < 0 ? nativeMax(length + index, 0) : nativeMin(index, length - 1); } return value === value ? strictLastIndexOf(array, value, index) : baseFindIndex(array, baseIsNaN, index, true); } /** * Gets the element at index `n` of `array`. If `n` is negative, the nth * element from the end is returned. * * @static * @memberOf _ * @since 4.11.0 * @category Array * @param {Array} array The array to query. * @param {number} [n=0] The index of the element to return. * @returns {*} Returns the nth element of `array`. * @example * * var array = ['a', 'b', 'c', 'd']; * * _.nth(array, 1); * // => 'b' * * _.nth(array, -2); * // => 'c'; */ function nth(array, n) { return (array && array.length) ? baseNth(array, toInteger(n)) : undefined; } /** * Removes all given values from `array` using * [`SameValueZero`](http://ecma-international.org/ecma-262/7.0/#sec-samevaluezero) * for equality comparisons. * * **Note:** Unlike `_.without`, this method mutates `array`. Use `_.remove` * to remove elements from an array by predicate. * * @static * @memberOf _ * @since 2.0.0 * @category Array * @param {Array} array The array to modify. * @param {...*} [values] The values to remove. * @returns {Array} Returns `array`. * @example * * var array = ['a', 'b', 'c', 'a', 'b', 'c']; * * _.pull(array, 'a', 'c'); * console.log(array); * // => ['b', 'b'] */ var pull = baseRest(pullAll); /** * This method is like `_.pull` except that it accepts an array of values to remove. * * **Note:** Unlike `_.difference`, this method mutates `array`. * * @static * @memberOf _ * @since 4.0.0 * @category Array * @param {Array} array The array to modify. * @param {Array} values The values to remove. * @returns {Array} Returns `array`. * @example * * var array = ['a', 'b', 'c', 'a', 'b', 'c']; * * _.pullAll(array, ['a', 'c']); * console.log(array); * // => ['b', 'b'] */ function pullAll(array, values) { return (array && array.length && values && values.length) ? basePullAll(array, values) : array; } /** * This method is like `_.pullAll` except that it accepts `iteratee` which is * invoked for each element of `array` and `values` to generate the criterion * by which they're compared. The iteratee is invoked with one argument: (value). * * **Note:** Unlike `_.differenceBy`, this method mutates `array`. * * @static * @memberOf _ * @since 4.0.0 * @category Array * @param {Array} array The array to modify. * @param {Array} values The values to remove. * @param {Function} [iteratee=_.identity] The iteratee invoked per element. * @returns {Array} Returns `array`. * @example * * var array = [{ 'x': 1 }, { 'x': 2 }, { 'x': 3 }, { 'x': 1 }]; * * _.pullAllBy(array, [{ 'x': 1 }, { 'x': 3 }], 'x'); * console.log(array); * // => [{ 'x': 2 }] */ function pullAllBy(array, values, iteratee) { return (array && array.length && values && values.length) ? basePullAll(array, values, getIteratee(iteratee, 2)) : array; } /** * This method is like `_.pullAll` except that it accepts `comparator` which * is invoked to compare elements of `array` to `values`. The comparator is * invoked with two arguments: (arrVal, othVal). * * **Note:** Unlike `_.differenceWith`, this method mutates `array`. * * @static * @memberOf _ * @since 4.6.0 * @category Array * @param {Array} array The array to modify. * @param {Array} values The values to remove. * @param {Function} [comparator] The comparator invoked per element. * @returns {Array} Returns `array`. * @example * * var array = [{ 'x': 1, 'y': 2 }, { 'x': 3, 'y': 4 }, { 'x': 5, 'y': 6 }]; * * _.pullAllWith(array, [{ 'x': 3, 'y': 4 }], _.isEqual); * console.log(array); * // => [{ 'x': 1, 'y': 2 }, { 'x': 5, 'y': 6 }] */ function pullAllWith(array, values, comparator) { return (array && array.length && values && values.length) ? basePullAll(array, values, undefined, comparator) : array; } /** * Removes elements from `array` corresponding to `indexes` and returns an * array of removed elements. * * **Note:** Unlike `_.at`, this method mutates `array`. * * @static * @memberOf _ * @since 3.0.0 * @category Array * @param {Array} array The array to modify. * @param {...(number|number[])} [indexes] The indexes of elements to remove. * @returns {Array} Returns the new array of removed elements. * @example * * var array = ['a', 'b', 'c', 'd']; * var pulled = _.pullAt(array, [1, 3]); * * console.log(array); * // => ['a', 'c'] * * console.log(pulled); * // => ['b', 'd'] */ var pullAt = flatRest(function(array, indexes) { var length = array == null ? 0 : array.length, result = baseAt(array, indexes); basePullAt(array, arrayMap(indexes, function(index) { return isIndex(index, length) ? +index : index; }).sort(compareAscending)); return result; }); /** * Removes all elements from `array` that `predicate` returns truthy for * and returns an array of the removed elements. The predicate is invoked * with three arguments: (value, index, array). * * **Note:** Unlike `_.filter`, this method mutates `array`. Use `_.pull` * to pull elements from an array by value. * * @static * @memberOf _ * @since 2.0.0 * @category Array * @param {Array} array The array to modify. * @param {Function} [predicate=_.identity] The function invoked per iteration. * @returns {Array} Returns the new array of removed elements. * @example * * var array = [1, 2, 3, 4]; * var evens = _.remove(array, function(n) { * return n % 2 == 0; * }); * * console.log(array); * // => [1, 3] * * console.log(evens); * // => [2, 4] */ function remove(array, predicate) { var result = []; if (!(array && array.length)) { return result; } var index = -1, indexes = [], length = array.length; predicate = getIteratee(predicate, 3); while (++index < length) { var value = array[index]; if (predicate(value, index, array)) { result.push(value); indexes.push(index); } } basePullAt(array, indexes); return result; } /** * Reverses `array` so that the first element becomes the last, the second * element becomes the second to last, and so on. * * **Note:** This method mutates `array` and is based on * [`Array#reverse`](https://mdn.io/Array/reverse). * * @static * @memberOf _ * @since 4.0.0 * @category Array * @param {Array} array The array to modify. * @returns {Array} Returns `array`. * @example * * var array = [1, 2, 3]; * * _.reverse(array); * // => [3, 2, 1] * * console.log(array); * // => [3, 2, 1] */ function reverse(array) { return array == null ? array : nativeReverse.call(array); } /** * Creates a slice of `array` from `start` up to, but not including, `end`. * * **Note:** This method is used instead of * [`Array#slice`](https://mdn.io/Array/slice) to ensure dense arrays are * returned. * * @static * @memberOf _ * @since 3.0.0 * @category Array * @param {Array} array The array to slice. * @param {number} [start=0] The start position. * @param {number} [end=array.length] The end position. * @returns {Array} Returns the slice of `array`. */ function slice(array, start, end) { var length = array == null ? 0 : array.length; if (!length) { return []; } if (end && typeof end != 'number' && isIterateeCall(array, start, end)) { start = 0; end = length; } else { start = start == null ? 0 : toInteger(start); end = end === undefined ? length : toInteger(end); } return baseSlice(array, start, end); } /** * Uses a binary search to determine the lowest index at which `value` * should be inserted into `array` in order to maintain its sort order. * * @static * @memberOf _ * @since 0.1.0 * @category Array * @param {Array} array The sorted array to inspect. * @param {*} value The value to evaluate. * @returns {number} Returns the index at which `value` should be inserted * into `array`. * @example * * _.sortedIndex([30, 50], 40); * // => 1 */ function sortedIndex(array, value) { return baseSortedIndex(array, value); } /** * This method is like `_.sortedIndex` except that it accepts `iteratee` * which is invoked for `value` and each element of `array` to compute their * sort ranking. The iteratee is invoked with one argument: (value). * * @static * @memberOf _ * @since 4.0.0 * @category Array * @param {Array} array The sorted array to inspect. * @param {*} value The value to evaluate. * @param {Function} [iteratee=_.identity] The iteratee invoked per element. * @returns {number} Returns the index at which `value` should be inserted * into `array`. * @example * * var objects = [{ 'x': 4 }, { 'x': 5 }]; * * _.sortedIndexBy(objects, { 'x': 4 }, function(o) { return o.x; }); * // => 0 * * // The `_.property` iteratee shorthand. * _.sortedIndexBy(objects, { 'x': 4 }, 'x'); * // => 0 */ function sortedIndexBy(array, value, iteratee) { return baseSortedIndexBy(array, value, getIteratee(iteratee, 2)); } /** * This method is like `_.indexOf` except that it performs a binary * search on a sorted `array`. * * @static * @memberOf _ * @since 4.0.0 * @category Array * @param {Array} array The array to inspect. * @param {*} value The value to search for. * @returns {number} Returns the index of the matched value, else `-1`. * @example * * _.sortedIndexOf([4, 5, 5, 5, 6], 5); * // => 1 */ function sortedIndexOf(array, value) { var length = array == null ? 0 : array.length; if (length) { var index = baseSortedIndex(array, value); if (index < length && eq(array[index], value)) { return index; } } return -1; } /** * This method is like `_.sortedIndex` except that it returns the highest * index at which `value` should be inserted into `array` in order to * maintain its sort order. * * @static * @memberOf _ * @since 3.0.0 * @category Array * @param {Array} array The sorted array to inspect. * @param {*} value The value to evaluate. * @returns {number} Returns the index at which `value` should be inserted * into `array`. * @example * * _.sortedLastIndex([4, 5, 5, 5, 6], 5); * // => 4 */ function sortedLastIndex(array, value) { return baseSortedIndex(array, value, true); } /** * This method is like `_.sortedLastIndex` except that it accepts `iteratee` * which is invoked for `value` and each element of `array` to compute their * sort ranking. The iteratee is invoked with one argument: (value). * * @static * @memberOf _ * @since 4.0.0 * @category Array * @param {Array} array The sorted array to inspect. * @param {*} value The value to evaluate. * @param {Function} [iteratee=_.identity] The iteratee invoked per element. * @returns {number} Returns the index at which `value` should be inserted * into `array`. * @example * * var objects = [{ 'x': 4 }, { 'x': 5 }]; * * _.sortedLastIndexBy(objects, { 'x': 4 }, function(o) { return o.x; }); * // => 1 * * // The `_.property` iteratee shorthand. * _.sortedLastIndexBy(objects, { 'x': 4 }, 'x'); * // => 1 */ function sortedLastIndexBy(array, value, iteratee) { return baseSortedIndexBy(array, value, getIteratee(iteratee, 2), true); } /** * This method is like `_.lastIndexOf` except that it performs a binary * search on a sorted `array`. * * @static * @memberOf _ * @since 4.0.0 * @category Array * @param {Array} array The array to inspect. * @param {*} value The value to search for. * @returns {number} Returns the index of the matched value, else `-1`. * @example * * _.sortedLastIndexOf([4, 5, 5, 5, 6], 5); * // => 3 */ function sortedLastIndexOf(array, value) { var length = array == null ? 0 : array.length; if (length) { var index = baseSortedIndex(array, value, true) - 1; if (eq(array[index], value)) { return index; } } return -1; } /** * This method is like `_.uniq` except that it's designed and optimized * for sorted arrays. * * @static * @memberOf _ * @since 4.0.0 * @category Array * @param {Array} array The array to inspect. * @returns {Array} Returns the new duplicate free array. * @example * * _.sortedUniq([1, 1, 2]); * // => [1, 2] */ function sortedUniq(array) { return (array && array.length) ? baseSortedUniq(array) : []; } /** * This method is like `_.uniqBy` except that it's designed and optimized * for sorted arrays. * * @static * @memberOf _ * @since 4.0.0 * @category Array * @param {Array} array The array to inspect. * @param {Function} [iteratee] The iteratee invoked per element. * @returns {Array} Returns the new duplicate free array. * @example * * _.sortedUniqBy([1.1, 1.2, 2.3, 2.4], Math.floor); * // => [1.1, 2.3] */ function sortedUniqBy(array, iteratee) { return (array && array.length) ? baseSortedUniq(array, getIteratee(iteratee, 2)) : []; } /** * Gets all but the first element of `array`. * * @static * @memberOf _ * @since 4.0.0 * @category Array * @param {Array} array The array to query. * @returns {Array} Returns the slice of `array`. * @example * * _.tail([1, 2, 3]); * // => [2, 3] */ function tail(array) { var length = array == null ? 0 : array.length; return length ? baseSlice(array, 1, length) : []; } /** * Creates a slice of `array` with `n` elements taken from the beginning. * * @static * @memberOf _ * @since 0.1.0 * @category Array * @param {Array} array The array to query. * @param {number} [n=1] The number of elements to take. * @param- {Object} [guard] Enables use as an iteratee for methods like `_.map`. * @returns {Array} Returns the slice of `array`. * @example * * _.take([1, 2, 3]); * // => [1] * * _.take([1, 2, 3], 2); * // => [1, 2] * * _.take([1, 2, 3], 5); * // => [1, 2, 3] * * _.take([1, 2, 3], 0); * // => [] */ function take(array, n, guard) { if (!(array && array.length)) { return []; } n = (guard || n === undefined) ? 1 : toInteger(n); return baseSlice(array, 0, n < 0 ? 0 : n); } /** * Creates a slice of `array` with `n` elements taken from the end. * * @static * @memberOf _ * @since 3.0.0 * @category Array * @param {Array} array The array to query. * @param {number} [n=1] The number of elements to take. * @param- {Object} [guard] Enables use as an iteratee for methods like `_.map`. * @returns {Array} Returns the slice of `array`. * @example * * _.takeRight([1, 2, 3]); * // => [3] * * _.takeRight([1, 2, 3], 2); * // => [2, 3] * * _.takeRight([1, 2, 3], 5); * // => [1, 2, 3] * * _.takeRight([1, 2, 3], 0); * // => [] */ function takeRight(array, n, guard) { var length = array == null ? 0 : array.length; if (!length) { return []; } n = (guard || n === undefined) ? 1 : toInteger(n); n = length - n; return baseSlice(array, n < 0 ? 0 : n, length); } /** * Creates a slice of `array` with elements taken from the end. Elements are * taken until `predicate` returns falsey. The predicate is invoked with * three arguments: (value, index, array). * * @static * @memberOf _ * @since 3.0.0 * @category Array * @param {Array} array The array to query. * @param {Function} [predicate=_.identity] The function invoked per iteration. * @returns {Array} Returns the slice of `array`. * @example * * var users = [ * { 'user': 'barney', 'active': true }, * { 'user': 'fred', 'active': false }, * { 'user': 'pebbles', 'active': false } * ]; * * _.takeRightWhile(users, function(o) { return !o.active; }); * // => objects for ['fred', 'pebbles'] * * // The `_.matches` iteratee shorthand. * _.takeRightWhile(users, { 'user': 'pebbles', 'active': false }); * // => objects for ['pebbles'] * * // The `_.matchesProperty` iteratee shorthand. * _.takeRightWhile(users, ['active', false]); * // => objects for ['fred', 'pebbles'] * * // The `_.property` iteratee shorthand. * _.takeRightWhile(users, 'active'); * // => [] */ function takeRightWhile(array, predicate) { return (array && array.length) ? baseWhile(array, getIteratee(predicate, 3), false, true) : []; } /** * Creates a slice of `array` with elements taken from the beginning. Elements * are taken until `predicate` returns falsey. The predicate is invoked with * three arguments: (value, index, array). * * @static * @memberOf _ * @since 3.0.0 * @category Array * @param {Array} array The array to query. * @param {Function} [predicate=_.identity] The function invoked per iteration. * @returns {Array} Returns the slice of `array`. * @example * * var users = [ * { 'user': 'barney', 'active': false }, * { 'user': 'fred', 'active': false }, * { 'user': 'pebbles', 'active': true } * ]; * * _.takeWhile(users, function(o) { return !o.active; }); * // => objects for ['barney', 'fred'] * * // The `_.matches` iteratee shorthand. * _.takeWhile(users, { 'user': 'barney', 'active': false }); * // => objects for ['barney'] * * // The `_.matchesProperty` iteratee shorthand. * _.takeWhile(users, ['active', false]); * // => objects for ['barney', 'fred'] * * // The `_.property` iteratee shorthand. * _.takeWhile(users, 'active'); * // => [] */ function takeWhile(array, predicate) { return (array && array.length) ? baseWhile(array, getIteratee(predicate, 3)) : []; } /** * Creates an array of unique values, in order, from all given arrays using * [`SameValueZero`](http://ecma-international.org/ecma-262/7.0/#sec-samevaluezero) * for equality comparisons. * * @static * @memberOf _ * @since 0.1.0 * @category Array * @param {...Array} [arrays] The arrays to inspect. * @returns {Array} Returns the new array of combined values. * @example * * _.union([2], [1, 2]); * // => [2, 1] */ var union = baseRest(function(arrays) { return baseUniq(baseFlatten(arrays, 1, isArrayLikeObject, true)); }); /** * This method is like `_.union` except that it accepts `iteratee` which is * invoked for each element of each `arrays` to generate the criterion by * which uniqueness is computed. Result values are chosen from the first * array in which the value occurs. The iteratee is invoked with one argument: * (value). * * @static * @memberOf _ * @since 4.0.0 * @category Array * @param {...Array} [arrays] The arrays to inspect. * @param {Function} [iteratee=_.identity] The iteratee invoked per element. * @returns {Array} Returns the new array of combined values. * @example * * _.unionBy([2.1], [1.2, 2.3], Math.floor); * // => [2.1, 1.2] * * // The `_.property` iteratee shorthand. * _.unionBy([{ 'x': 1 }], [{ 'x': 2 }, { 'x': 1 }], 'x'); * // => [{ 'x': 1 }, { 'x': 2 }] */ var unionBy = baseRest(function(arrays) { var iteratee = last(arrays); if (isArrayLikeObject(iteratee)) { iteratee = undefined; } return baseUniq(baseFlatten(arrays, 1, isArrayLikeObject, true), getIteratee(iteratee, 2)); }); /** * This method is like `_.union` except that it accepts `comparator` which * is invoked to compare elements of `arrays`. Result values are chosen from * the first array in which the value occurs. The comparator is invoked * with two arguments: (arrVal, othVal). * * @static * @memberOf _ * @since 4.0.0 * @category Array * @param {...Array} [arrays] The arrays to inspect. * @param {Function} [comparator] The comparator invoked per element. * @returns {Array} Returns the new array of combined values. * @example * * var objects = [{ 'x': 1, 'y': 2 }, { 'x': 2, 'y': 1 }]; * var others = [{ 'x': 1, 'y': 1 }, { 'x': 1, 'y': 2 }]; * * _.unionWith(objects, others, _.isEqual); * // => [{ 'x': 1, 'y': 2 }, { 'x': 2, 'y': 1 }, { 'x': 1, 'y': 1 }] */ var unionWith = baseRest(function(arrays) { var comparator = last(arrays); comparator = typeof comparator == 'function' ? comparator : undefined; return baseUniq(baseFlatten(arrays, 1, isArrayLikeObject, true), undefined, comparator); }); /** * Creates a duplicate-free version of an array, using * [`SameValueZero`](http://ecma-international.org/ecma-262/7.0/#sec-samevaluezero) * for equality comparisons, in which only the first occurrence of each element * is kept. The order of result values is determined by the order they occur * in the array. * * @static * @memberOf _ * @since 0.1.0 * @category Array * @param {Array} array The array to inspect. * @returns {Array} Returns the new duplicate free array. * @example * * _.uniq([2, 1, 2]); * // => [2, 1] */ function uniq(array) { return (array && array.length) ? baseUniq(array) : []; } /** * This method is like `_.uniq` except that it accepts `iteratee` which is * invoked for each element in `array` to generate the criterion by which * uniqueness is computed. The order of result values is determined by the * order they occur in the array. The iteratee is invoked with one argument: * (value). * * @static * @memberOf _ * @since 4.0.0 * @category Array * @param {Array} array The array to inspect. * @param {Function} [iteratee=_.identity] The iteratee invoked per element. * @returns {Array} Returns the new duplicate free array. * @example * * _.uniqBy([2.1, 1.2, 2.3], Math.floor); * // => [2.1, 1.2] * * // The `_.property` iteratee shorthand. * _.uniqBy([{ 'x': 1 }, { 'x': 2 }, { 'x': 1 }], 'x'); * // => [{ 'x': 1 }, { 'x': 2 }] */ function uniqBy(array, iteratee) { return (array && array.length) ? baseUniq(array, getIteratee(iteratee, 2)) : []; } /** * This method is like `_.uniq` except that it accepts `comparator` which * is invoked to compare elements of `array`. The order of result values is * determined by the order they occur in the array.The comparator is invoked * with two arguments: (arrVal, othVal). * * @static * @memberOf _ * @since 4.0.0 * @category Array * @param {Array} array The array to inspect. * @param {Function} [comparator] The comparator invoked per element. * @returns {Array} Returns the new duplicate free array. * @example * * var objects = [{ 'x': 1, 'y': 2 }, { 'x': 2, 'y': 1 }, { 'x': 1, 'y': 2 }]; * * _.uniqWith(objects, _.isEqual); * // => [{ 'x': 1, 'y': 2 }, { 'x': 2, 'y': 1 }] */ function uniqWith(array, comparator) { comparator = typeof comparator == 'function' ? comparator : undefined; return (array && array.length) ? baseUniq(array, undefined, comparator) : []; } /** * This method is like `_.zip` except that it accepts an array of grouped * elements and creates an array regrouping the elements to their pre-zip * configuration. * * @static * @memberOf _ * @since 1.2.0 * @category Array * @param {Array} array The array of grouped elements to process. * @returns {Array} Returns the new array of regrouped elements. * @example * * var zipped = _.zip(['a', 'b'], [1, 2], [true, false]); * // => [['a', 1, true], ['b', 2, false]] * * _.unzip(zipped); * // => [['a', 'b'], [1, 2], [true, false]] */ function unzip(array) { if (!(array && array.length)) { return []; } var length = 0; array = arrayFilter(array, function(group) { if (isArrayLikeObject(group)) { length = nativeMax(group.length, length); return true; } }); return baseTimes(length, function(index) { return arrayMap(array, baseProperty(index)); }); } /** * This method is like `_.unzip` except that it accepts `iteratee` to specify * how regrouped values should be combined. The iteratee is invoked with the * elements of each group: (...group). * * @static * @memberOf _ * @since 3.8.0 * @category Array * @param {Array} array The array of grouped elements to process. * @param {Function} [iteratee=_.identity] The function to combine * regrouped values. * @returns {Array} Returns the new array of regrouped elements. * @example * * var zipped = _.zip([1, 2], [10, 20], [100, 200]); * // => [[1, 10, 100], [2, 20, 200]] * * _.unzipWith(zipped, _.add); * // => [3, 30, 300] */ function unzipWith(array, iteratee) { if (!(array && array.length)) { return []; } var result = unzip(array); if (iteratee == null) { return result; } return arrayMap(result, function(group) { return apply(iteratee, undefined, group); }); } /** * Creates an array excluding all given values using * [`SameValueZero`](http://ecma-international.org/ecma-262/7.0/#sec-samevaluezero) * for equality comparisons. * * **Note:** Unlike `_.pull`, this method returns a new array. * * @static * @memberOf _ * @since 0.1.0 * @category Array * @param {Array} array The array to inspect. * @param {...*} [values] The values to exclude. * @returns {Array} Returns the new array of filtered values. * @see _.difference, _.xor * @example * * _.without([2, 1, 2, 3], 1, 2); * // => [3] */ var without = baseRest(function(array, values) { return isArrayLikeObject(array) ? baseDifference(array, values) : []; }); /** * Creates an array of unique values that is the * [symmetric difference](https://en.wikipedia.org/wiki/Symmetric_difference) * of the given arrays. The order of result values is determined by the order * they occur in the arrays. * * @static * @memberOf _ * @since 2.4.0 * @category Array * @param {...Array} [arrays] The arrays to inspect. * @returns {Array} Returns the new array of filtered values. * @see _.difference, _.without * @example * * _.xor([2, 1], [2, 3]); * // => [1, 3] */ var xor = baseRest(function(arrays) { return baseXor(arrayFilter(arrays, isArrayLikeObject)); }); /** * This method is like `_.xor` except that it accepts `iteratee` which is * invoked for each element of each `arrays` to generate the criterion by * which by which they're compared. The order of result values is determined * by the order they occur in the arrays. The iteratee is invoked with one * argument: (value). * * @static * @memberOf _ * @since 4.0.0 * @category Array * @param {...Array} [arrays] The arrays to inspect. * @param {Function} [iteratee=_.identity] The iteratee invoked per element. * @returns {Array} Returns the new array of filtered values. * @example * * _.xorBy([2.1, 1.2], [2.3, 3.4], Math.floor); * // => [1.2, 3.4] * * // The `_.property` iteratee shorthand. * _.xorBy([{ 'x': 1 }], [{ 'x': 2 }, { 'x': 1 }], 'x'); * // => [{ 'x': 2 }] */ var xorBy = baseRest(function(arrays) { var iteratee = last(arrays); if (isArrayLikeObject(iteratee)) { iteratee = undefined; } return baseXor(arrayFilter(arrays, isArrayLikeObject), getIteratee(iteratee, 2)); }); /** * This method is like `_.xor` except that it accepts `comparator` which is * invoked to compare elements of `arrays`. The order of result values is * determined by the order they occur in the arrays. The comparator is invoked * with two arguments: (arrVal, othVal). * * @static * @memberOf _ * @since 4.0.0 * @category Array * @param {...Array} [arrays] The arrays to inspect. * @param {Function} [comparator] The comparator invoked per element. * @returns {Array} Returns the new array of filtered values. * @example * * var objects = [{ 'x': 1, 'y': 2 }, { 'x': 2, 'y': 1 }]; * var others = [{ 'x': 1, 'y': 1 }, { 'x': 1, 'y': 2 }]; * * _.xorWith(objects, others, _.isEqual); * // => [{ 'x': 2, 'y': 1 }, { 'x': 1, 'y': 1 }] */ var xorWith = baseRest(function(arrays) { var comparator = last(arrays); comparator = typeof comparator == 'function' ? comparator : undefined; return baseXor(arrayFilter(arrays, isArrayLikeObject), undefined, comparator); }); /** * Creates an array of grouped elements, the first of which contains the * first elements of the given arrays, the second of which contains the * second elements of the given arrays, and so on. * * @static * @memberOf _ * @since 0.1.0 * @category Array * @param {...Array} [arrays] The arrays to process. * @returns {Array} Returns the new array of grouped elements. * @example * * _.zip(['a', 'b'], [1, 2], [true, false]); * // => [['a', 1, true], ['b', 2, false]] */ var zip = baseRest(unzip); /** * This method is like `_.fromPairs` except that it accepts two arrays, * one of property identifiers and one of corresponding values. * * @static * @memberOf _ * @since 0.4.0 * @category Array * @param {Array} [props=[]] The property identifiers. * @param {Array} [values=[]] The property values. * @returns {Object} Returns the new object. * @example * * _.zipObject(['a', 'b'], [1, 2]); * // => { 'a': 1, 'b': 2 } */ function zipObject(props, values) { return baseZipObject(props || [], values || [], assignValue); } /** * This method is like `_.zipObject` except that it supports property paths. * * @static * @memberOf _ * @since 4.1.0 * @category Array * @param {Array} [props=[]] The property identifiers. * @param {Array} [values=[]] The property values. * @returns {Object} Returns the new object. * @example * * _.zipObjectDeep(['a.b[0].c', 'a.b[1].d'], [1, 2]); * // => { 'a': { 'b': [{ 'c': 1 }, { 'd': 2 }] } } */ function zipObjectDeep(props, values) { return baseZipObject(props || [], values || [], baseSet); } /** * This method is like `_.zip` except that it accepts `iteratee` to specify * how grouped values should be combined. The iteratee is invoked with the * elements of each group: (...group). * * @static * @memberOf _ * @since 3.8.0 * @category Array * @param {...Array} [arrays] The arrays to process. * @param {Function} [iteratee=_.identity] The function to combine * grouped values. * @returns {Array} Returns the new array of grouped elements. * @example * * _.zipWith([1, 2], [10, 20], [100, 200], function(a, b, c) { * return a + b + c; * }); * // => [111, 222] */ var zipWith = baseRest(function(arrays) { var length = arrays.length, iteratee = length > 1 ? arrays[length - 1] : undefined; iteratee = typeof iteratee == 'function' ? (arrays.pop(), iteratee) : undefined; return unzipWith(arrays, iteratee); }); /*------------------------------------------------------------------------*/ /** * Creates a `lodash` wrapper instance that wraps `value` with explicit method * chain sequences enabled. The result of such sequences must be unwrapped * with `_#value`. * * @static * @memberOf _ * @since 1.3.0 * @category Seq * @param {*} value The value to wrap. * @returns {Object} Returns the new `lodash` wrapper instance. * @example * * var users = [ * { 'user': 'barney', 'age': 36 }, * { 'user': 'fred', 'age': 40 }, * { 'user': 'pebbles', 'age': 1 } * ]; * * var youngest = _ * .chain(users) * .sortBy('age') * .map(function(o) { * return o.user + ' is ' + o.age; * }) * .head() * .value(); * // => 'pebbles is 1' */ function chain(value) { var result = lodash(value); result.__chain__ = true; return result; } /** * This method invokes `interceptor` and returns `value`. The interceptor * is invoked with one argument; (value). The purpose of this method is to * "tap into" a method chain sequence in order to modify intermediate results. * * @static * @memberOf _ * @since 0.1.0 * @category Seq * @param {*} value The value to provide to `interceptor`. * @param {Function} interceptor The function to invoke. * @returns {*} Returns `value`. * @example * * _([1, 2, 3]) * .tap(function(array) { * // Mutate input array. * array.pop(); * }) * .reverse() * .value(); * // => [2, 1] */ function tap(value, interceptor) { interceptor(value); return value; } /** * This method is like `_.tap` except that it returns the result of `interceptor`. * The purpose of this method is to "pass thru" values replacing intermediate * results in a method chain sequence. * * @static * @memberOf _ * @since 3.0.0 * @category Seq * @param {*} value The value to provide to `interceptor`. * @param {Function} interceptor The function to invoke. * @returns {*} Returns the result of `interceptor`. * @example * * _(' abc ') * .chain() * .trim() * .thru(function(value) { * return [value]; * }) * .value(); * // => ['abc'] */ function thru(value, interceptor) { return interceptor(value); } /** * This method is the wrapper version of `_.at`. * * @name at * @memberOf _ * @since 1.0.0 * @category Seq * @param {...(string|string[])} [paths] The property paths to pick. * @returns {Object} Returns the new `lodash` wrapper instance. * @example * * var object = { 'a': [{ 'b': { 'c': 3 } }, 4] }; * * _(object).at(['a[0].b.c', 'a[1]']).value(); * // => [3, 4] */ var wrapperAt = flatRest(function(paths) { var length = paths.length, start = length ? paths[0] : 0, value = this.__wrapped__, interceptor = function(object) { return baseAt(object, paths); }; if (length > 1 || this.__actions__.length || !(value instanceof LazyWrapper) || !isIndex(start)) { return this.thru(interceptor); } value = value.slice(start, +start + (length ? 1 : 0)); value.__actions__.push({ 'func': thru, 'args': [interceptor], 'thisArg': undefined }); return new LodashWrapper(value, this.__chain__).thru(function(array) { if (length && !array.length) { array.push(undefined); } return array; }); }); /** * Creates a `lodash` wrapper instance with explicit method chain sequences enabled. * * @name chain * @memberOf _ * @since 0.1.0 * @category Seq * @returns {Object} Returns the new `lodash` wrapper instance. * @example * * var users = [ * { 'user': 'barney', 'age': 36 }, * { 'user': 'fred', 'age': 40 } * ]; * * // A sequence without explicit chaining. * _(users).head(); * // => { 'user': 'barney', 'age': 36 } * * // A sequence with explicit chaining. * _(users) * .chain() * .head() * .pick('user') * .value(); * // => { 'user': 'barney' } */ function wrapperChain() { return chain(this); } /** * Executes the chain sequence and returns the wrapped result. * * @name commit * @memberOf _ * @since 3.2.0 * @category Seq * @returns {Object} Returns the new `lodash` wrapper instance. * @example * * var array = [1, 2]; * var wrapped = _(array).push(3); * * console.log(array); * // => [1, 2] * * wrapped = wrapped.commit(); * console.log(array); * // => [1, 2, 3] * * wrapped.last(); * // => 3 * * console.log(array); * // => [1, 2, 3] */ function wrapperCommit() { return new LodashWrapper(this.value(), this.__chain__); } /** * Gets the next value on a wrapped object following the * [iterator protocol](https://mdn.io/iteration_protocols#iterator). * * @name next * @memberOf _ * @since 4.0.0 * @category Seq * @returns {Object} Returns the next iterator value. * @example * * var wrapped = _([1, 2]); * * wrapped.next(); * // => { 'done': false, 'value': 1 } * * wrapped.next(); * // => { 'done': false, 'value': 2 } * * wrapped.next(); * // => { 'done': true, 'value': undefined } */ function wrapperNext() { if (this.__values__ === undefined) { this.__values__ = toArray(this.value()); } var done = this.__index__ >= this.__values__.length, value = done ? undefined : this.__values__[this.__index__++]; return { 'done': done, 'value': value }; } /** * Enables the wrapper to be iterable. * * @name Symbol.iterator * @memberOf _ * @since 4.0.0 * @category Seq * @returns {Object} Returns the wrapper object. * @example * * var wrapped = _([1, 2]); * * wrapped[Symbol.iterator]() === wrapped; * // => true * * Array.from(wrapped); * // => [1, 2] */ function wrapperToIterator() { return this; } /** * Creates a clone of the chain sequence planting `value` as the wrapped value. * * @name plant * @memberOf _ * @since 3.2.0 * @category Seq * @param {*} value The value to plant. * @returns {Object} Returns the new `lodash` wrapper instance. * @example * * function square(n) { * return n * n; * } * * var wrapped = _([1, 2]).map(square); * var other = wrapped.plant([3, 4]); * * other.value(); * // => [9, 16] * * wrapped.value(); * // => [1, 4] */ function wrapperPlant(value) { var result, parent = this; while (parent instanceof baseLodash) { var clone = wrapperClone(parent); clone.__index__ = 0; clone.__values__ = undefined; if (result) { previous.__wrapped__ = clone; } else { result = clone; } var previous = clone; parent = parent.__wrapped__; } previous.__wrapped__ = value; return result; } /** * This method is the wrapper version of `_.reverse`. * * **Note:** This method mutates the wrapped array. * * @name reverse * @memberOf _ * @since 0.1.0 * @category Seq * @returns {Object} Returns the new `lodash` wrapper instance. * @example * * var array = [1, 2, 3]; * * _(array).reverse().value() * // => [3, 2, 1] * * console.log(array); * // => [3, 2, 1] */ function wrapperReverse() { var value = this.__wrapped__; if (value instanceof LazyWrapper) { var wrapped = value; if (this.__actions__.length) { wrapped = new LazyWrapper(this); } wrapped = wrapped.reverse(); wrapped.__actions__.push({ 'func': thru, 'args': [reverse], 'thisArg': undefined }); return new LodashWrapper(wrapped, this.__chain__); } return this.thru(reverse); } /** * Executes the chain sequence to resolve the unwrapped value. * * @name value * @memberOf _ * @since 0.1.0 * @alias toJSON, valueOf * @category Seq * @returns {*} Returns the resolved unwrapped value. * @example * * _([1, 2, 3]).value(); * // => [1, 2, 3] */ function wrapperValue() { return baseWrapperValue(this.__wrapped__, this.__actions__); } /*------------------------------------------------------------------------*/ /** * Creates an object composed of keys generated from the results of running * each element of `collection` thru `iteratee`. The corresponding value of * each key is the number of times the key was returned by `iteratee`. The * iteratee is invoked with one argument: (value). * * @static * @memberOf _ * @since 0.5.0 * @category Collection * @param {Array|Object} collection The collection to iterate over. * @param {Function} [iteratee=_.identity] The iteratee to transform keys. * @returns {Object} Returns the composed aggregate object. * @example * * _.countBy([6.1, 4.2, 6.3], Math.floor); * // => { '4': 1, '6': 2 } * * // The `_.property` iteratee shorthand. * _.countBy(['one', 'two', 'three'], 'length'); * // => { '3': 2, '5': 1 } */ var countBy = createAggregator(function(result, value, key) { if (hasOwnProperty.call(result, key)) { ++result[key]; } else { baseAssignValue(result, key, 1); } }); /** * Checks if `predicate` returns truthy for **all** elements of `collection`. * Iteration is stopped once `predicate` returns falsey. The predicate is * invoked with three arguments: (value, index|key, collection). * * **Note:** This method returns `true` for * [empty collections](https://en.wikipedia.org/wiki/Empty_set) because * [everything is true](https://en.wikipedia.org/wiki/Vacuous_truth) of * elements of empty collections. * * @static * @memberOf _ * @since 0.1.0 * @category Collection * @param {Array|Object} collection The collection to iterate over. * @param {Function} [predicate=_.identity] The function invoked per iteration. * @param- {Object} [guard] Enables use as an iteratee for methods like `_.map`. * @returns {boolean} Returns `true` if all elements pass the predicate check, * else `false`. * @example * * _.every([true, 1, null, 'yes'], Boolean); * // => false * * var users = [ * { 'user': 'barney', 'age': 36, 'active': false }, * { 'user': 'fred', 'age': 40, 'active': false } * ]; * * // The `_.matches` iteratee shorthand. * _.every(users, { 'user': 'barney', 'active': false }); * // => false * * // The `_.matchesProperty` iteratee shorthand. * _.every(users, ['active', false]); * // => true * * // The `_.property` iteratee shorthand. * _.every(users, 'active'); * // => false */ function every(collection, predicate, guard) { var func = isArray(collection) ? arrayEvery : baseEvery; if (guard && isIterateeCall(collection, predicate, guard)) { predicate = undefined; } return func(collection, getIteratee(predicate, 3)); } /** * Iterates over elements of `collection`, returning an array of all elements * `predicate` returns truthy for. The predicate is invoked with three * arguments: (value, index|key, collection). * * **Note:** Unlike `_.remove`, this method returns a new array. * * @static * @memberOf _ * @since 0.1.0 * @category Collection * @param {Array|Object} collection The collection to iterate over. * @param {Function} [predicate=_.identity] The function invoked per iteration. * @returns {Array} Returns the new filtered array. * @see _.reject * @example * * var users = [ * { 'user': 'barney', 'age': 36, 'active': true }, * { 'user': 'fred', 'age': 40, 'active': false } * ]; * * _.filter(users, function(o) { return !o.active; }); * // => objects for ['fred'] * * // The `_.matches` iteratee shorthand. * _.filter(users, { 'age': 36, 'active': true }); * // => objects for ['barney'] * * // The `_.matchesProperty` iteratee shorthand. * _.filter(users, ['active', false]); * // => objects for ['fred'] * * // The `_.property` iteratee shorthand. * _.filter(users, 'active'); * // => objects for ['barney'] * * // Combining several predicates using `_.overEvery` or `_.overSome`. * _.filter(users, _.overSome([{ 'age': 36 }, ['age', 40]])); * // => objects for ['fred', 'barney'] */ function filter(collection, predicate) { var func = isArray(collection) ? arrayFilter : baseFilter; return func(collection, getIteratee(predicate, 3)); } /** * Iterates over elements of `collection`, returning the first element * `predicate` returns truthy for. The predicate is invoked with three * arguments: (value, index|key, collection). * * @static * @memberOf _ * @since 0.1.0 * @category Collection * @param {Array|Object} collection The collection to inspect. * @param {Function} [predicate=_.identity] The function invoked per iteration. * @param {number} [fromIndex=0] The index to search from. * @returns {*} Returns the matched element, else `undefined`. * @example * * var users = [ * { 'user': 'barney', 'age': 36, 'active': true }, * { 'user': 'fred', 'age': 40, 'active': false }, * { 'user': 'pebbles', 'age': 1, 'active': true } * ]; * * _.find(users, function(o) { return o.age < 40; }); * // => object for 'barney' * * // The `_.matches` iteratee shorthand. * _.find(users, { 'age': 1, 'active': true }); * // => object for 'pebbles' * * // The `_.matchesProperty` iteratee shorthand. * _.find(users, ['active', false]); * // => object for 'fred' * * // The `_.property` iteratee shorthand. * _.find(users, 'active'); * // => object for 'barney' */ var find = createFind(findIndex); /** * This method is like `_.find` except that it iterates over elements of * `collection` from right to left. * * @static * @memberOf _ * @since 2.0.0 * @category Collection * @param {Array|Object} collection The collection to inspect. * @param {Function} [predicate=_.identity] The function invoked per iteration. * @param {number} [fromIndex=collection.length-1] The index to search from. * @returns {*} Returns the matched element, else `undefined`. * @example * * _.findLast([1, 2, 3, 4], function(n) { * return n % 2 == 1; * }); * // => 3 */ var findLast = createFind(findLastIndex); /** * Creates a flattened array of values by running each element in `collection` * thru `iteratee` and flattening the mapped results. The iteratee is invoked * with three arguments: (value, index|key, collection). * * @static * @memberOf _ * @since 4.0.0 * @category Collection * @param {Array|Object} collection The collection to iterate over. * @param {Function} [iteratee=_.identity] The function invoked per iteration. * @returns {Array} Returns the new flattened array. * @example * * function duplicate(n) { * return [n, n]; * } * * _.flatMap([1, 2], duplicate); * // => [1, 1, 2, 2] */ function flatMap(collection, iteratee) { return baseFlatten(map(collection, iteratee), 1); } /** * This method is like `_.flatMap` except that it recursively flattens the * mapped results. * * @static * @memberOf _ * @since 4.7.0 * @category Collection * @param {Array|Object} collection The collection to iterate over. * @param {Function} [iteratee=_.identity] The function invoked per iteration. * @returns {Array} Returns the new flattened array. * @example * * function duplicate(n) { * return [[[n, n]]]; * } * * _.flatMapDeep([1, 2], duplicate); * // => [1, 1, 2, 2] */ function flatMapDeep(collection, iteratee) { return baseFlatten(map(collection, iteratee), INFINITY); } /** * This method is like `_.flatMap` except that it recursively flattens the * mapped results up to `depth` times. * * @static * @memberOf _ * @since 4.7.0 * @category Collection * @param {Array|Object} collection The collection to iterate over. * @param {Function} [iteratee=_.identity] The function invoked per iteration. * @param {number} [depth=1] The maximum recursion depth. * @returns {Array} Returns the new flattened array. * @example * * function duplicate(n) { * return [[[n, n]]]; * } * * _.flatMapDepth([1, 2], duplicate, 2); * // => [[1, 1], [2, 2]] */ function flatMapDepth(collection, iteratee, depth) { depth = depth === undefined ? 1 : toInteger(depth); return baseFlatten(map(collection, iteratee), depth); } /** * Iterates over elements of `collection` and invokes `iteratee` for each element. * The iteratee is invoked with three arguments: (value, index|key, collection). * Iteratee functions may exit iteration early by explicitly returning `false`. * * **Note:** As with other "Collections" methods, objects with a "length" * property are iterated like arrays. To avoid this behavior use `_.forIn` * or `_.forOwn` for object iteration. * * @static * @memberOf _ * @since 0.1.0 * @alias each * @category Collection * @param {Array|Object} collection The collection to iterate over. * @param {Function} [iteratee=_.identity] The function invoked per iteration. * @returns {Array|Object} Returns `collection`. * @see _.forEachRight * @example * * _.forEach([1, 2], function(value) { * console.log(value); * }); * // => Logs `1` then `2`. * * _.forEach({ 'a': 1, 'b': 2 }, function(value, key) { * console.log(key); * }); * // => Logs 'a' then 'b' (iteration order is not guaranteed). */ function forEach(collection, iteratee) { var func = isArray(collection) ? arrayEach : baseEach; return func(collection, getIteratee(iteratee, 3)); } /** * This method is like `_.forEach` except that it iterates over elements of * `collection` from right to left. * * @static * @memberOf _ * @since 2.0.0 * @alias eachRight * @category Collection * @param {Array|Object} collection The collection to iterate over. * @param {Function} [iteratee=_.identity] The function invoked per iteration. * @returns {Array|Object} Returns `collection`. * @see _.forEach * @example * * _.forEachRight([1, 2], function(value) { * console.log(value); * }); * // => Logs `2` then `1`. */ function forEachRight(collection, iteratee) { var func = isArray(collection) ? arrayEachRight : baseEachRight; return func(collection, getIteratee(iteratee, 3)); } /** * Creates an object composed of keys generated from the results of running * each element of `collection` thru `iteratee`. The order of grouped values * is determined by the order they occur in `collection`. The corresponding * value of each key is an array of elements responsible for generating the * key. The iteratee is invoked with one argument: (value). * * @static * @memberOf _ * @since 0.1.0 * @category Collection * @param {Array|Object} collection The collection to iterate over. * @param {Function} [iteratee=_.identity] The iteratee to transform keys. * @returns {Object} Returns the composed aggregate object. * @example * * _.groupBy([6.1, 4.2, 6.3], Math.floor); * // => { '4': [4.2], '6': [6.1, 6.3] } * * // The `_.property` iteratee shorthand. * _.groupBy(['one', 'two', 'three'], 'length'); * // => { '3': ['one', 'two'], '5': ['three'] } */ var groupBy = createAggregator(function(result, value, key) { if (hasOwnProperty.call(result, key)) { result[key].push(value); } else { baseAssignValue(result, key, [value]); } }); /** * Checks if `value` is in `collection`. If `collection` is a string, it's * checked for a substring of `value`, otherwise * [`SameValueZero`](http://ecma-international.org/ecma-262/7.0/#sec-samevaluezero) * is used for equality comparisons. If `fromIndex` is negative, it's used as * the offset from the end of `collection`. * * @static * @memberOf _ * @since 0.1.0 * @category Collection * @param {Array|Object|string} collection The collection to inspect. * @param {*} value The value to search for. * @param {number} [fromIndex=0] The index to search from. * @param- {Object} [guard] Enables use as an iteratee for methods like `_.reduce`. * @returns {boolean} Returns `true` if `value` is found, else `false`. * @example * * _.includes([1, 2, 3], 1); * // => true * * _.includes([1, 2, 3], 1, 2); * // => false * * _.includes({ 'a': 1, 'b': 2 }, 1); * // => true * * _.includes('abcd', 'bc'); * // => true */ function includes(collection, value, fromIndex, guard) { collection = isArrayLike(collection) ? collection : values(collection); fromIndex = (fromIndex && !guard) ? toInteger(fromIndex) : 0; var length = collection.length; if (fromIndex < 0) { fromIndex = nativeMax(length + fromIndex, 0); } return isString(collection) ? (fromIndex <= length && collection.indexOf(value, fromIndex) > -1) : (!!length && baseIndexOf(collection, value, fromIndex) > -1); } /** * Invokes the method at `path` of each element in `collection`, returning * an array of the results of each invoked method. Any additional arguments * are provided to each invoked method. If `path` is a function, it's invoked * for, and `this` bound to, each element in `collection`. * * @static * @memberOf _ * @since 4.0.0 * @category Collection * @param {Array|Object} collection The collection to iterate over. * @param {Array|Function|string} path The path of the method to invoke or * the function invoked per iteration. * @param {...*} [args] The arguments to invoke each method with. * @returns {Array} Returns the array of results. * @example * * _.invokeMap([[5, 1, 7], [3, 2, 1]], 'sort'); * // => [[1, 5, 7], [1, 2, 3]] * * _.invokeMap([123, 456], String.prototype.split, ''); * // => [['1', '2', '3'], ['4', '5', '6']] */ var invokeMap = baseRest(function(collection, path, args) { var index = -1, isFunc = typeof path == 'function', result = isArrayLike(collection) ? Array(collection.length) : []; baseEach(collection, function(value) { result[++index] = isFunc ? apply(path, value, args) : baseInvoke(value, path, args); }); return result; }); /** * Creates an object composed of keys generated from the results of running * each element of `collection` thru `iteratee`. The corresponding value of * each key is the last element responsible for generating the key. The * iteratee is invoked with one argument: (value). * * @static * @memberOf _ * @since 4.0.0 * @category Collection * @param {Array|Object} collection The collection to iterate over. * @param {Function} [iteratee=_.identity] The iteratee to transform keys. * @returns {Object} Returns the composed aggregate object. * @example * * var array = [ * { 'dir': 'left', 'code': 97 }, * { 'dir': 'right', 'code': 100 } * ]; * * _.keyBy(array, function(o) { * return String.fromCharCode(o.code); * }); * // => { 'a': { 'dir': 'left', 'code': 97 }, 'd': { 'dir': 'right', 'code': 100 } } * * _.keyBy(array, 'dir'); * // => { 'left': { 'dir': 'left', 'code': 97 }, 'right': { 'dir': 'right', 'code': 100 } } */ var keyBy = createAggregator(function(result, value, key) { baseAssignValue(result, key, value); }); /** * Creates an array of values by running each element in `collection` thru * `iteratee`. The iteratee is invoked with three arguments: * (value, index|key, collection). * * Many lodash methods are guarded to work as iteratees for methods like * `_.every`, `_.filter`, `_.map`, `_.mapValues`, `_.reject`, and `_.some`. * * The guarded methods are: * `ary`, `chunk`, `curry`, `curryRight`, `drop`, `dropRight`, `every`, * `fill`, `invert`, `parseInt`, `random`, `range`, `rangeRight`, `repeat`, * `sampleSize`, `slice`, `some`, `sortBy`, `split`, `take`, `takeRight`, * `template`, `trim`, `trimEnd`, `trimStart`, and `words` * * @static * @memberOf _ * @since 0.1.0 * @category Collection * @param {Array|Object} collection The collection to iterate over. * @param {Function} [iteratee=_.identity] The function invoked per iteration. * @returns {Array} Returns the new mapped array. * @example * * function square(n) { * return n * n; * } * * _.map([4, 8], square); * // => [16, 64] * * _.map({ 'a': 4, 'b': 8 }, square); * // => [16, 64] (iteration order is not guaranteed) * * var users = [ * { 'user': 'barney' }, * { 'user': 'fred' } * ]; * * // The `_.property` iteratee shorthand. * _.map(users, 'user'); * // => ['barney', 'fred'] */ function map(collection, iteratee) { var func = isArray(collection) ? arrayMap : baseMap; return func(collection, getIteratee(iteratee, 3)); } /** * This method is like `_.sortBy` except that it allows specifying the sort * orders of the iteratees to sort by. If `orders` is unspecified, all values * are sorted in ascending order. Otherwise, specify an order of "desc" for * descending or "asc" for ascending sort order of corresponding values. * * @static * @memberOf _ * @since 4.0.0 * @category Collection * @param {Array|Object} collection The collection to iterate over. * @param {Array[]|Function[]|Object[]|string[]} [iteratees=[_.identity]] * The iteratees to sort by. * @param {string[]} [orders] The sort orders of `iteratees`. * @param- {Object} [guard] Enables use as an iteratee for methods like `_.reduce`. * @returns {Array} Returns the new sorted array. * @example * * var users = [ * { 'user': 'fred', 'age': 48 }, * { 'user': 'barney', 'age': 34 }, * { 'user': 'fred', 'age': 40 }, * { 'user': 'barney', 'age': 36 } * ]; * * // Sort by `user` in ascending order and by `age` in descending order. * _.orderBy(users, ['user', 'age'], ['asc', 'desc']); * // => objects for [['barney', 36], ['barney', 34], ['fred', 48], ['fred', 40]] */ function orderBy(collection, iteratees, orders, guard) { if (collection == null) { return []; } if (!isArray(iteratees)) { iteratees = iteratees == null ? [] : [iteratees]; } orders = guard ? undefined : orders; if (!isArray(orders)) { orders = orders == null ? [] : [orders]; } return baseOrderBy(collection, iteratees, orders); } /** * Creates an array of elements split into two groups, the first of which * contains elements `predicate` returns truthy for, the second of which * contains elements `predicate` returns falsey for. The predicate is * invoked with one argument: (value). * * @static * @memberOf _ * @since 3.0.0 * @category Collection * @param {Array|Object} collection The collection to iterate over. * @param {Function} [predicate=_.identity] The function invoked per iteration. * @returns {Array} Returns the array of grouped elements. * @example * * var users = [ * { 'user': 'barney', 'age': 36, 'active': false }, * { 'user': 'fred', 'age': 40, 'active': true }, * { 'user': 'pebbles', 'age': 1, 'active': false } * ]; * * _.partition(users, function(o) { return o.active; }); * // => objects for [['fred'], ['barney', 'pebbles']] * * // The `_.matches` iteratee shorthand. * _.partition(users, { 'age': 1, 'active': false }); * // => objects for [['pebbles'], ['barney', 'fred']] * * // The `_.matchesProperty` iteratee shorthand. * _.partition(users, ['active', false]); * // => objects for [['barney', 'pebbles'], ['fred']] * * // The `_.property` iteratee shorthand. * _.partition(users, 'active'); * // => objects for [['fred'], ['barney', 'pebbles']] */ var partition = createAggregator(function(result, value, key) { result[key ? 0 : 1].push(value); }, function() { return [[], []]; }); /** * Reduces `collection` to a value which is the accumulated result of running * each element in `collection` thru `iteratee`, where each successive * invocation is supplied the return value of the previous. If `accumulator` * is not given, the first element of `collection` is used as the initial * value. The iteratee is invoked with four arguments: * (accumulator, value, index|key, collection). * * Many lodash methods are guarded to work as iteratees for methods like * `_.reduce`, `_.reduceRight`, and `_.transform`. * * The guarded methods are: * `assign`, `defaults`, `defaultsDeep`, `includes`, `merge`, `orderBy`, * and `sortBy` * * @static * @memberOf _ * @since 0.1.0 * @category Collection * @param {Array|Object} collection The collection to iterate over. * @param {Function} [iteratee=_.identity] The function invoked per iteration. * @param {*} [accumulator] The initial value. * @returns {*} Returns the accumulated value. * @see _.reduceRight * @example * * _.reduce([1, 2], function(sum, n) { * return sum + n; * }, 0); * // => 3 * * _.reduce({ 'a': 1, 'b': 2, 'c': 1 }, function(result, value, key) { * (result[value] || (result[value] = [])).push(key); * return result; * }, {}); * // => { '1': ['a', 'c'], '2': ['b'] } (iteration order is not guaranteed) */ function reduce(collection, iteratee, accumulator) { var func = isArray(collection) ? arrayReduce : baseReduce, initAccum = arguments.length < 3; return func(collection, getIteratee(iteratee, 4), accumulator, initAccum, baseEach); } /** * This method is like `_.reduce` except that it iterates over elements of * `collection` from right to left. * * @static * @memberOf _ * @since 0.1.0 * @category Collection * @param {Array|Object} collection The collection to iterate over. * @param {Function} [iteratee=_.identity] The function invoked per iteration. * @param {*} [accumulator] The initial value. * @returns {*} Returns the accumulated value. * @see _.reduce * @example * * var array = [[0, 1], [2, 3], [4, 5]]; * * _.reduceRight(array, function(flattened, other) { * return flattened.concat(other); * }, []); * // => [4, 5, 2, 3, 0, 1] */ function reduceRight(collection, iteratee, accumulator) { var func = isArray(collection) ? arrayReduceRight : baseReduce, initAccum = arguments.length < 3; return func(collection, getIteratee(iteratee, 4), accumulator, initAccum, baseEachRight); } /** * The opposite of `_.filter`; this method returns the elements of `collection` * that `predicate` does **not** return truthy for. * * @static * @memberOf _ * @since 0.1.0 * @category Collection * @param {Array|Object} collection The collection to iterate over. * @param {Function} [predicate=_.identity] The function invoked per iteration. * @returns {Array} Returns the new filtered array. * @see _.filter * @example * * var users = [ * { 'user': 'barney', 'age': 36, 'active': false }, * { 'user': 'fred', 'age': 40, 'active': true } * ]; * * _.reject(users, function(o) { return !o.active; }); * // => objects for ['fred'] * * // The `_.matches` iteratee shorthand. * _.reject(users, { 'age': 40, 'active': true }); * // => objects for ['barney'] * * // The `_.matchesProperty` iteratee shorthand. * _.reject(users, ['active', false]); * // => objects for ['fred'] * * // The `_.property` iteratee shorthand. * _.reject(users, 'active'); * // => objects for ['barney'] */ function reject(collection, predicate) { var func = isArray(collection) ? arrayFilter : baseFilter; return func(collection, negate(getIteratee(predicate, 3))); } /** * Gets a random element from `collection`. * * @static * @memberOf _ * @since 2.0.0 * @category Collection * @param {Array|Object} collection The collection to sample. * @returns {*} Returns the random element. * @example * * _.sample([1, 2, 3, 4]); * // => 2 */ function sample(collection) { var func = isArray(collection) ? arraySample : baseSample; return func(collection); } /** * Gets `n` random elements at unique keys from `collection` up to the * size of `collection`. * * @static * @memberOf _ * @since 4.0.0 * @category Collection * @param {Array|Object} collection The collection to sample. * @param {number} [n=1] The number of elements to sample. * @param- {Object} [guard] Enables use as an iteratee for methods like `_.map`. * @returns {Array} Returns the random elements. * @example * * _.sampleSize([1, 2, 3], 2); * // => [3, 1] * * _.sampleSize([1, 2, 3], 4); * // => [2, 3, 1] */ function sampleSize(collection, n, guard) { if ((guard ? isIterateeCall(collection, n, guard) : n === undefined)) { n = 1; } else { n = toInteger(n); } var func = isArray(collection) ? arraySampleSize : baseSampleSize; return func(collection, n); } /** * Creates an array of shuffled values, using a version of the * [Fisher-Yates shuffle](https://en.wikipedia.org/wiki/Fisher-Yates_shuffle). * * @static * @memberOf _ * @since 0.1.0 * @category Collection * @param {Array|Object} collection The collection to shuffle. * @returns {Array} Returns the new shuffled array. * @example * * _.shuffle([1, 2, 3, 4]); * // => [4, 1, 3, 2] */ function shuffle(collection) { var func = isArray(collection) ? arrayShuffle : baseShuffle; return func(collection); } /** * Gets the size of `collection` by returning its length for array-like * values or the number of own enumerable string keyed properties for objects. * * @static * @memberOf _ * @since 0.1.0 * @category Collection * @param {Array|Object|string} collection The collection to inspect. * @returns {number} Returns the collection size. * @example * * _.size([1, 2, 3]); * // => 3 * * _.size({ 'a': 1, 'b': 2 }); * // => 2 * * _.size('pebbles'); * // => 7 */ function size(collection) { if (collection == null) { return 0; } if (isArrayLike(collection)) { return isString(collection) ? stringSize(collection) : collection.length; } var tag = getTag(collection); if (tag == mapTag || tag == setTag) { return collection.size; } return baseKeys(collection).length; } /** * Checks if `predicate` returns truthy for **any** element of `collection`. * Iteration is stopped once `predicate` returns truthy. The predicate is * invoked with three arguments: (value, index|key, collection). * * @static * @memberOf _ * @since 0.1.0 * @category Collection * @param {Array|Object} collection The collection to iterate over. * @param {Function} [predicate=_.identity] The function invoked per iteration. * @param- {Object} [guard] Enables use as an iteratee for methods like `_.map`. * @returns {boolean} Returns `true` if any element passes the predicate check, * else `false`. * @example * * _.some([null, 0, 'yes', false], Boolean); * // => true * * var users = [ * { 'user': 'barney', 'active': true }, * { 'user': 'fred', 'active': false } * ]; * * // The `_.matches` iteratee shorthand. * _.some(users, { 'user': 'barney', 'active': false }); * // => false * * // The `_.matchesProperty` iteratee shorthand. * _.some(users, ['active', false]); * // => true * * // The `_.property` iteratee shorthand. * _.some(users, 'active'); * // => true */ function some(collection, predicate, guard) { var func = isArray(collection) ? arraySome : baseSome; if (guard && isIterateeCall(collection, predicate, guard)) { predicate = undefined; } return func(collection, getIteratee(predicate, 3)); } /** * Creates an array of elements, sorted in ascending order by the results of * running each element in a collection thru each iteratee. This method * performs a stable sort, that is, it preserves the original sort order of * equal elements. The iteratees are invoked with one argument: (value). * * @static * @memberOf _ * @since 0.1.0 * @category Collection * @param {Array|Object} collection The collection to iterate over. * @param {...(Function|Function[])} [iteratees=[_.identity]] * The iteratees to sort by. * @returns {Array} Returns the new sorted array. * @example * * var users = [ * { 'user': 'fred', 'age': 48 }, * { 'user': 'barney', 'age': 36 }, * { 'user': 'fred', 'age': 30 }, * { 'user': 'barney', 'age': 34 } * ]; * * _.sortBy(users, [function(o) { return o.user; }]); * // => objects for [['barney', 36], ['barney', 34], ['fred', 48], ['fred', 30]] * * _.sortBy(users, ['user', 'age']); * // => objects for [['barney', 34], ['barney', 36], ['fred', 30], ['fred', 48]] */ var sortBy = baseRest(function(collection, iteratees) { if (collection == null) { return []; } var length = iteratees.length; if (length > 1 && isIterateeCall(collection, iteratees[0], iteratees[1])) { iteratees = []; } else if (length > 2 && isIterateeCall(iteratees[0], iteratees[1], iteratees[2])) { iteratees = [iteratees[0]]; } return baseOrderBy(collection, baseFlatten(iteratees, 1), []); }); /*------------------------------------------------------------------------*/ /** * Gets the timestamp of the number of milliseconds that have elapsed since * the Unix epoch (1 January 1970 00:00:00 UTC). * * @static * @memberOf _ * @since 2.4.0 * @category Date * @returns {number} Returns the timestamp. * @example * * _.defer(function(stamp) { * console.log(_.now() - stamp); * }, _.now()); * // => Logs the number of milliseconds it took for the deferred invocation. */ var now = ctxNow || function() { return root.Date.now(); }; /*------------------------------------------------------------------------*/ /** * The opposite of `_.before`; this method creates a function that invokes * `func` once it's called `n` or more times. * * @static * @memberOf _ * @since 0.1.0 * @category Function * @param {number} n The number of calls before `func` is invoked. * @param {Function} func The function to restrict. * @returns {Function} Returns the new restricted function. * @example * * var saves = ['profile', 'settings']; * * var done = _.after(saves.length, function() { * console.log('done saving!'); * }); * * _.forEach(saves, function(type) { * asyncSave({ 'type': type, 'complete': done }); * }); * // => Logs 'done saving!' after the two async saves have completed. */ function after(n, func) { if (typeof func != 'function') { throw new TypeError(FUNC_ERROR_TEXT); } n = toInteger(n); return function() { if (--n < 1) { return func.apply(this, arguments); } }; } /** * Creates a function that invokes `func`, with up to `n` arguments, * ignoring any additional arguments. * * @static * @memberOf _ * @since 3.0.0 * @category Function * @param {Function} func The function to cap arguments for. * @param {number} [n=func.length] The arity cap. * @param- {Object} [guard] Enables use as an iteratee for methods like `_.map`. * @returns {Function} Returns the new capped function. * @example * * _.map(['6', '8', '10'], _.ary(parseInt, 1)); * // => [6, 8, 10] */ function ary(func, n, guard) { n = guard ? undefined : n; n = (func && n == null) ? func.length : n; return createWrap(func, WRAP_ARY_FLAG, undefined, undefined, undefined, undefined, n); } /** * Creates a function that invokes `func`, with the `this` binding and arguments * of the created function, while it's called less than `n` times. Subsequent * calls to the created function return the result of the last `func` invocation. * * @static * @memberOf _ * @since 3.0.0 * @category Function * @param {number} n The number of calls at which `func` is no longer invoked. * @param {Function} func The function to restrict. * @returns {Function} Returns the new restricted function. * @example * * jQuery(element).on('click', _.before(5, addContactToList)); * // => Allows adding up to 4 contacts to the list. */ function before(n, func) { var result; if (typeof func != 'function') { throw new TypeError(FUNC_ERROR_TEXT); } n = toInteger(n); return function() { if (--n > 0) { result = func.apply(this, arguments); } if (n <= 1) { func = undefined; } return result; }; } /** * Creates a function that invokes `func` with the `this` binding of `thisArg` * and `partials` prepended to the arguments it receives. * * The `_.bind.placeholder` value, which defaults to `_` in monolithic builds, * may be used as a placeholder for partially applied arguments. * * **Note:** Unlike native `Function#bind`, this method doesn't set the "length" * property of bound functions. * * @static * @memberOf _ * @since 0.1.0 * @category Function * @param {Function} func The function to bind. * @param {*} thisArg The `this` binding of `func`. * @param {...*} [partials] The arguments to be partially applied. * @returns {Function} Returns the new bound function. * @example * * function greet(greeting, punctuation) { * return greeting + ' ' + this.user + punctuation; * } * * var object = { 'user': 'fred' }; * * var bound = _.bind(greet, object, 'hi'); * bound('!'); * // => 'hi fred!' * * // Bound with placeholders. * var bound = _.bind(greet, object, _, '!'); * bound('hi'); * // => 'hi fred!' */ var bind = baseRest(function(func, thisArg, partials) { var bitmask = WRAP_BIND_FLAG; if (partials.length) { var holders = replaceHolders(partials, getHolder(bind)); bitmask |= WRAP_PARTIAL_FLAG; } return createWrap(func, bitmask, thisArg, partials, holders); }); /** * Creates a function that invokes the method at `object[key]` with `partials` * prepended to the arguments it receives. * * This method differs from `_.bind` by allowing bound functions to reference * methods that may be redefined or don't yet exist. See * [Peter Michaux's article](http://peter.michaux.ca/articles/lazy-function-definition-pattern) * for more details. * * The `_.bindKey.placeholder` value, which defaults to `_` in monolithic * builds, may be used as a placeholder for partially applied arguments. * * @static * @memberOf _ * @since 0.10.0 * @category Function * @param {Object} object The object to invoke the method on. * @param {string} key The key of the method. * @param {...*} [partials] The arguments to be partially applied. * @returns {Function} Returns the new bound function. * @example * * var object = { * 'user': 'fred', * 'greet': function(greeting, punctuation) { * return greeting + ' ' + this.user + punctuation; * } * }; * * var bound = _.bindKey(object, 'greet', 'hi'); * bound('!'); * // => 'hi fred!' * * object.greet = function(greeting, punctuation) { * return greeting + 'ya ' + this.user + punctuation; * }; * * bound('!'); * // => 'hiya fred!' * * // Bound with placeholders. * var bound = _.bindKey(object, 'greet', _, '!'); * bound('hi'); * // => 'hiya fred!' */ var bindKey = baseRest(function(object, key, partials) { var bitmask = WRAP_BIND_FLAG | WRAP_BIND_KEY_FLAG; if (partials.length) { var holders = replaceHolders(partials, getHolder(bindKey)); bitmask |= WRAP_PARTIAL_FLAG; } return createWrap(key, bitmask, object, partials, holders); }); /** * Creates a function that accepts arguments of `func` and either invokes * `func` returning its result, if at least `arity` number of arguments have * been provided, or returns a function that accepts the remaining `func` * arguments, and so on. The arity of `func` may be specified if `func.length` * is not sufficient. * * The `_.curry.placeholder` value, which defaults to `_` in monolithic builds, * may be used as a placeholder for provided arguments. * * **Note:** This method doesn't set the "length" property of curried functions. * * @static * @memberOf _ * @since 2.0.0 * @category Function * @param {Function} func The function to curry. * @param {number} [arity=func.length] The arity of `func`. * @param- {Object} [guard] Enables use as an iteratee for methods like `_.map`. * @returns {Function} Returns the new curried function. * @example * * var abc = function(a, b, c) { * return [a, b, c]; * }; * * var curried = _.curry(abc); * * curried(1)(2)(3); * // => [1, 2, 3] * * curried(1, 2)(3); * // => [1, 2, 3] * * curried(1, 2, 3); * // => [1, 2, 3] * * // Curried with placeholders. * curried(1)(_, 3)(2); * // => [1, 2, 3] */ function curry(func, arity, guard) { arity = guard ? undefined : arity; var result = createWrap(func, WRAP_CURRY_FLAG, undefined, undefined, undefined, undefined, undefined, arity); result.placeholder = curry.placeholder; return result; } /** * This method is like `_.curry` except that arguments are applied to `func` * in the manner of `_.partialRight` instead of `_.partial`. * * The `_.curryRight.placeholder` value, which defaults to `_` in monolithic * builds, may be used as a placeholder for provided arguments. * * **Note:** This method doesn't set the "length" property of curried functions. * * @static * @memberOf _ * @since 3.0.0 * @category Function * @param {Function} func The function to curry. * @param {number} [arity=func.length] The arity of `func`. * @param- {Object} [guard] Enables use as an iteratee for methods like `_.map`. * @returns {Function} Returns the new curried function. * @example * * var abc = function(a, b, c) { * return [a, b, c]; * }; * * var curried = _.curryRight(abc); * * curried(3)(2)(1); * // => [1, 2, 3] * * curried(2, 3)(1); * // => [1, 2, 3] * * curried(1, 2, 3); * // => [1, 2, 3] * * // Curried with placeholders. * curried(3)(1, _)(2); * // => [1, 2, 3] */ function curryRight(func, arity, guard) { arity = guard ? undefined : arity; var result = createWrap(func, WRAP_CURRY_RIGHT_FLAG, undefined, undefined, undefined, undefined, undefined, arity); result.placeholder = curryRight.placeholder; return result; } /** * Creates a debounced function that delays invoking `func` until after `wait` * milliseconds have elapsed since the last time the debounced function was * invoked. The debounced function comes with a `cancel` method to cancel * delayed `func` invocations and a `flush` method to immediately invoke them. * Provide `options` to indicate whether `func` should be invoked on the * leading and/or trailing edge of the `wait` timeout. The `func` is invoked * with the last arguments provided to the debounced function. Subsequent * calls to the debounced function return the result of the last `func` * invocation. * * **Note:** If `leading` and `trailing` options are `true`, `func` is * invoked on the trailing edge of the timeout only if the debounced function * is invoked more than once during the `wait` timeout. * * If `wait` is `0` and `leading` is `false`, `func` invocation is deferred * until to the next tick, similar to `setTimeout` with a timeout of `0`. * * See [David Corbacho's article](https://css-tricks.com/debouncing-throttling-explained-examples/) * for details over the differences between `_.debounce` and `_.throttle`. * * @static * @memberOf _ * @since 0.1.0 * @category Function * @param {Function} func The function to debounce. * @param {number} [wait=0] The number of milliseconds to delay. * @param {Object} [options={}] The options object. * @param {boolean} [options.leading=false] * Specify invoking on the leading edge of the timeout. * @param {number} [options.maxWait] * The maximum time `func` is allowed to be delayed before it's invoked. * @param {boolean} [options.trailing=true] * Specify invoking on the trailing edge of the timeout. * @returns {Function} Returns the new debounced function. * @example * * // Avoid costly calculations while the window size is in flux. * jQuery(window).on('resize', _.debounce(calculateLayout, 150)); * * // Invoke `sendMail` when clicked, debouncing subsequent calls. * jQuery(element).on('click', _.debounce(sendMail, 300, { * 'leading': true, * 'trailing': false * })); * * // Ensure `batchLog` is invoked once after 1 second of debounced calls. * var debounced = _.debounce(batchLog, 250, { 'maxWait': 1000 }); * var source = new EventSource('/stream'); * jQuery(source).on('message', debounced); * * // Cancel the trailing debounced invocation. * jQuery(window).on('popstate', debounced.cancel); */ function debounce(func, wait, options) { var lastArgs, lastThis, maxWait, result, timerId, lastCallTime, lastInvokeTime = 0, leading = false, maxing = false, trailing = true; if (typeof func != 'function') { throw new TypeError(FUNC_ERROR_TEXT); } wait = toNumber(wait) || 0; if (isObject(options)) { leading = !!options.leading; maxing = 'maxWait' in options; maxWait = maxing ? nativeMax(toNumber(options.maxWait) || 0, wait) : maxWait; trailing = 'trailing' in options ? !!options.trailing : trailing; } function invokeFunc(time) { var args = lastArgs, thisArg = lastThis; lastArgs = lastThis = undefined; lastInvokeTime = time; result = func.apply(thisArg, args); return result; } function leadingEdge(time) { // Reset any `maxWait` timer. lastInvokeTime = time; // Start the timer for the trailing edge. timerId = setTimeout(timerExpired, wait); // Invoke the leading edge. return leading ? invokeFunc(time) : result; } function remainingWait(time) { var timeSinceLastCall = time - lastCallTime, timeSinceLastInvoke = time - lastInvokeTime, timeWaiting = wait - timeSinceLastCall; return maxing ? nativeMin(timeWaiting, maxWait - timeSinceLastInvoke) : timeWaiting; } function shouldInvoke(time) { var timeSinceLastCall = time - lastCallTime, timeSinceLastInvoke = time - lastInvokeTime; // Either this is the first call, activity has stopped and we're at the // trailing edge, the system time has gone backwards and we're treating // it as the trailing edge, or we've hit the `maxWait` limit. return (lastCallTime === undefined || (timeSinceLastCall >= wait) || (timeSinceLastCall < 0) || (maxing && timeSinceLastInvoke >= maxWait)); } function timerExpired() { var time = now(); if (shouldInvoke(time)) { return trailingEdge(time); } // Restart the timer. timerId = setTimeout(timerExpired, remainingWait(time)); } function trailingEdge(time) { timerId = undefined; // Only invoke if we have `lastArgs` which means `func` has been // debounced at least once. if (trailing && lastArgs) { return invokeFunc(time); } lastArgs = lastThis = undefined; return result; } function cancel() { if (timerId !== undefined) { clearTimeout(timerId); } lastInvokeTime = 0; lastArgs = lastCallTime = lastThis = timerId = undefined; } function flush() { return timerId === undefined ? result : trailingEdge(now()); } function debounced() { var time = now(), isInvoking = shouldInvoke(time); lastArgs = arguments; lastThis = this; lastCallTime = time; if (isInvoking) { if (timerId === undefined) { return leadingEdge(lastCallTime); } if (maxing) { // Handle invocations in a tight loop. clearTimeout(timerId); timerId = setTimeout(timerExpired, wait); return invokeFunc(lastCallTime); } } if (timerId === undefined) { timerId = setTimeout(timerExpired, wait); } return result; } debounced.cancel = cancel; debounced.flush = flush; return debounced; } /** * Defers invoking the `func` until the current call stack has cleared. Any * additional arguments are provided to `func` when it's invoked. * * @static * @memberOf _ * @since 0.1.0 * @category Function * @param {Function} func The function to defer. * @param {...*} [args] The arguments to invoke `func` with. * @returns {number} Returns the timer id. * @example * * _.defer(function(text) { * console.log(text); * }, 'deferred'); * // => Logs 'deferred' after one millisecond. */ var defer = baseRest(function(func, args) { return baseDelay(func, 1, args); }); /** * Invokes `func` after `wait` milliseconds. Any additional arguments are * provided to `func` when it's invoked. * * @static * @memberOf _ * @since 0.1.0 * @category Function * @param {Function} func The function to delay. * @param {number} wait The number of milliseconds to delay invocation. * @param {...*} [args] The arguments to invoke `func` with. * @returns {number} Returns the timer id. * @example * * _.delay(function(text) { * console.log(text); * }, 1000, 'later'); * // => Logs 'later' after one second. */ var delay = baseRest(function(func, wait, args) { return baseDelay(func, toNumber(wait) || 0, args); }); /** * Creates a function that invokes `func` with arguments reversed. * * @static * @memberOf _ * @since 4.0.0 * @category Function * @param {Function} func The function to flip arguments for. * @returns {Function} Returns the new flipped function. * @example * * var flipped = _.flip(function() { * return _.toArray(arguments); * }); * * flipped('a', 'b', 'c', 'd'); * // => ['d', 'c', 'b', 'a'] */ function flip(func) { return createWrap(func, WRAP_FLIP_FLAG); } /** * Creates a function that memoizes the result of `func`. If `resolver` is * provided, it determines the cache key for storing the result based on the * arguments provided to the memoized function. By default, the first argument * provided to the memoized function is used as the map cache key. The `func` * is invoked with the `this` binding of the memoized function. * * **Note:** The cache is exposed as the `cache` property on the memoized * function. Its creation may be customized by replacing the `_.memoize.Cache` * constructor with one whose instances implement the * [`Map`](http://ecma-international.org/ecma-262/7.0/#sec-properties-of-the-map-prototype-object) * method interface of `clear`, `delete`, `get`, `has`, and `set`. * * @static * @memberOf _ * @since 0.1.0 * @category Function * @param {Function} func The function to have its output memoized. * @param {Function} [resolver] The function to resolve the cache key. * @returns {Function} Returns the new memoized function. * @example * * var object = { 'a': 1, 'b': 2 }; * var other = { 'c': 3, 'd': 4 }; * * var values = _.memoize(_.values); * values(object); * // => [1, 2] * * values(other); * // => [3, 4] * * object.a = 2; * values(object); * // => [1, 2] * * // Modify the result cache. * values.cache.set(object, ['a', 'b']); * values(object); * // => ['a', 'b'] * * // Replace `_.memoize.Cache`. * _.memoize.Cache = WeakMap; */ function memoize(func, resolver) { if (typeof func != 'function' || (resolver != null && typeof resolver != 'function')) { throw new TypeError(FUNC_ERROR_TEXT); } var memoized = function() { var args = arguments, key = resolver ? resolver.apply(this, args) : args[0], cache = memoized.cache; if (cache.has(key)) { return cache.get(key); } var result = func.apply(this, args); memoized.cache = cache.set(key, result) || cache; return result; }; memoized.cache = new (memoize.Cache || MapCache); return memoized; } // Expose `MapCache`. memoize.Cache = MapCache; /** * Creates a function that negates the result of the predicate `func`. The * `func` predicate is invoked with the `this` binding and arguments of the * created function. * * @static * @memberOf _ * @since 3.0.0 * @category Function * @param {Function} predicate The predicate to negate. * @returns {Function} Returns the new negated function. * @example * * function isEven(n) { * return n % 2 == 0; * } * * _.filter([1, 2, 3, 4, 5, 6], _.negate(isEven)); * // => [1, 3, 5] */ function negate(predicate) { if (typeof predicate != 'function') { throw new TypeError(FUNC_ERROR_TEXT); } return function() { var args = arguments; switch (args.length) { case 0: return !predicate.call(this); case 1: return !predicate.call(this, args[0]); case 2: return !predicate.call(this, args[0], args[1]); case 3: return !predicate.call(this, args[0], args[1], args[2]); } return !predicate.apply(this, args); }; } /** * Creates a function that is restricted to invoking `func` once. Repeat calls * to the function return the value of the first invocation. The `func` is * invoked with the `this` binding and arguments of the created function. * * @static * @memberOf _ * @since 0.1.0 * @category Function * @param {Function} func The function to restrict. * @returns {Function} Returns the new restricted function. * @example * * var initialize = _.once(createApplication); * initialize(); * initialize(); * // => `createApplication` is invoked once */ function once(func) { return before(2, func); } /** * Creates a function that invokes `func` with its arguments transformed. * * @static * @since 4.0.0 * @memberOf _ * @category Function * @param {Function} func The function to wrap. * @param {...(Function|Function[])} [transforms=[_.identity]] * The argument transforms. * @returns {Function} Returns the new function. * @example * * function doubled(n) { * return n * 2; * } * * function square(n) { * return n * n; * } * * var func = _.overArgs(function(x, y) { * return [x, y]; * }, [square, doubled]); * * func(9, 3); * // => [81, 6] * * func(10, 5); * // => [100, 10] */ var overArgs = castRest(function(func, transforms) { transforms = (transforms.length == 1 && isArray(transforms[0])) ? arrayMap(transforms[0], baseUnary(getIteratee())) : arrayMap(baseFlatten(transforms, 1), baseUnary(getIteratee())); var funcsLength = transforms.length; return baseRest(function(args) { var index = -1, length = nativeMin(args.length, funcsLength); while (++index < length) { args[index] = transforms[index].call(this, args[index]); } return apply(func, this, args); }); }); /** * Creates a function that invokes `func` with `partials` prepended to the * arguments it receives. This method is like `_.bind` except it does **not** * alter the `this` binding. * * The `_.partial.placeholder` value, which defaults to `_` in monolithic * builds, may be used as a placeholder for partially applied arguments. * * **Note:** This method doesn't set the "length" property of partially * applied functions. * * @static * @memberOf _ * @since 0.2.0 * @category Function * @param {Function} func The function to partially apply arguments to. * @param {...*} [partials] The arguments to be partially applied. * @returns {Function} Returns the new partially applied function. * @example * * function greet(greeting, name) { * return greeting + ' ' + name; * } * * var sayHelloTo = _.partial(greet, 'hello'); * sayHelloTo('fred'); * // => 'hello fred' * * // Partially applied with placeholders. * var greetFred = _.partial(greet, _, 'fred'); * greetFred('hi'); * // => 'hi fred' */ var partial = baseRest(function(func, partials) { var holders = replaceHolders(partials, getHolder(partial)); return createWrap(func, WRAP_PARTIAL_FLAG, undefined, partials, holders); }); /** * This method is like `_.partial` except that partially applied arguments * are appended to the arguments it receives. * * The `_.partialRight.placeholder` value, which defaults to `_` in monolithic * builds, may be used as a placeholder for partially applied arguments. * * **Note:** This method doesn't set the "length" property of partially * applied functions. * * @static * @memberOf _ * @since 1.0.0 * @category Function * @param {Function} func The function to partially apply arguments to. * @param {...*} [partials] The arguments to be partially applied. * @returns {Function} Returns the new partially applied function. * @example * * function greet(greeting, name) { * return greeting + ' ' + name; * } * * var greetFred = _.partialRight(greet, 'fred'); * greetFred('hi'); * // => 'hi fred' * * // Partially applied with placeholders. * var sayHelloTo = _.partialRight(greet, 'hello', _); * sayHelloTo('fred'); * // => 'hello fred' */ var partialRight = baseRest(function(func, partials) { var holders = replaceHolders(partials, getHolder(partialRight)); return createWrap(func, WRAP_PARTIAL_RIGHT_FLAG, undefined, partials, holders); }); /** * Creates a function that invokes `func` with arguments arranged according * to the specified `indexes` where the argument value at the first index is * provided as the first argument, the argument value at the second index is * provided as the second argument, and so on. * * @static * @memberOf _ * @since 3.0.0 * @category Function * @param {Function} func The function to rearrange arguments for. * @param {...(number|number[])} indexes The arranged argument indexes. * @returns {Function} Returns the new function. * @example * * var rearged = _.rearg(function(a, b, c) { * return [a, b, c]; * }, [2, 0, 1]); * * rearged('b', 'c', 'a') * // => ['a', 'b', 'c'] */ var rearg = flatRest(function(func, indexes) { return createWrap(func, WRAP_REARG_FLAG, undefined, undefined, undefined, indexes); }); /** * Creates a function that invokes `func` with the `this` binding of the * created function and arguments from `start` and beyond provided as * an array. * * **Note:** This method is based on the * [rest parameter](https://mdn.io/rest_parameters). * * @static * @memberOf _ * @since 4.0.0 * @category Function * @param {Function} func The function to apply a rest parameter to. * @param {number} [start=func.length-1] The start position of the rest parameter. * @returns {Function} Returns the new function. * @example * * var say = _.rest(function(what, names) { * return what + ' ' + _.initial(names).join(', ') + * (_.size(names) > 1 ? ', & ' : '') + _.last(names); * }); * * say('hello', 'fred', 'barney', 'pebbles'); * // => 'hello fred, barney, & pebbles' */ function rest(func, start) { if (typeof func != 'function') { throw new TypeError(FUNC_ERROR_TEXT); } start = start === undefined ? start : toInteger(start); return baseRest(func, start); } /** * Creates a function that invokes `func` with the `this` binding of the * create function and an array of arguments much like * [`Function#apply`](http://www.ecma-international.org/ecma-262/7.0/#sec-function.prototype.apply). * * **Note:** This method is based on the * [spread operator](https://mdn.io/spread_operator). * * @static * @memberOf _ * @since 3.2.0 * @category Function * @param {Function} func The function to spread arguments over. * @param {number} [start=0] The start position of the spread. * @returns {Function} Returns the new function. * @example * * var say = _.spread(function(who, what) { * return who + ' says ' + what; * }); * * say(['fred', 'hello']); * // => 'fred says hello' * * var numbers = Promise.all([ * Promise.resolve(40), * Promise.resolve(36) * ]); * * numbers.then(_.spread(function(x, y) { * return x + y; * })); * // => a Promise of 76 */ function spread(func, start) { if (typeof func != 'function') { throw new TypeError(FUNC_ERROR_TEXT); } start = start == null ? 0 : nativeMax(toInteger(start), 0); return baseRest(function(args) { var array = args[start], otherArgs = castSlice(args, 0, start); if (array) { arrayPush(otherArgs, array); } return apply(func, this, otherArgs); }); } /** * Creates a throttled function that only invokes `func` at most once per * every `wait` milliseconds. The throttled function comes with a `cancel` * method to cancel delayed `func` invocations and a `flush` method to * immediately invoke them. Provide `options` to indicate whether `func` * should be invoked on the leading and/or trailing edge of the `wait` * timeout. The `func` is invoked with the last arguments provided to the * throttled function. Subsequent calls to the throttled function return the * result of the last `func` invocation. * * **Note:** If `leading` and `trailing` options are `true`, `func` is * invoked on the trailing edge of the timeout only if the throttled function * is invoked more than once during the `wait` timeout. * * If `wait` is `0` and `leading` is `false`, `func` invocation is deferred * until to the next tick, similar to `setTimeout` with a timeout of `0`. * * See [David Corbacho's article](https://css-tricks.com/debouncing-throttling-explained-examples/) * for details over the differences between `_.throttle` and `_.debounce`. * * @static * @memberOf _ * @since 0.1.0 * @category Function * @param {Function} func The function to throttle. * @param {number} [wait=0] The number of milliseconds to throttle invocations to. * @param {Object} [options={}] The options object. * @param {boolean} [options.leading=true] * Specify invoking on the leading edge of the timeout. * @param {boolean} [options.trailing=true] * Specify invoking on the trailing edge of the timeout. * @returns {Function} Returns the new throttled function. * @example * * // Avoid excessively updating the position while scrolling. * jQuery(window).on('scroll', _.throttle(updatePosition, 100)); * * // Invoke `renewToken` when the click event is fired, but not more than once every 5 minutes. * var throttled = _.throttle(renewToken, 300000, { 'trailing': false }); * jQuery(element).on('click', throttled); * * // Cancel the trailing throttled invocation. * jQuery(window).on('popstate', throttled.cancel); */ function throttle(func, wait, options) { var leading = true, trailing = true; if (typeof func != 'function') { throw new TypeError(FUNC_ERROR_TEXT); } if (isObject(options)) { leading = 'leading' in options ? !!options.leading : leading; trailing = 'trailing' in options ? !!options.trailing : trailing; } return debounce(func, wait, { 'leading': leading, 'maxWait': wait, 'trailing': trailing }); } /** * Creates a function that accepts up to one argument, ignoring any * additional arguments. * * @static * @memberOf _ * @since 4.0.0 * @category Function * @param {Function} func The function to cap arguments for. * @returns {Function} Returns the new capped function. * @example * * _.map(['6', '8', '10'], _.unary(parseInt)); * // => [6, 8, 10] */ function unary(func) { return ary(func, 1); } /** * Creates a function that provides `value` to `wrapper` as its first * argument. Any additional arguments provided to the function are appended * to those provided to the `wrapper`. The wrapper is invoked with the `this` * binding of the created function. * * @static * @memberOf _ * @since 0.1.0 * @category Function * @param {*} value The value to wrap. * @param {Function} [wrapper=identity] The wrapper function. * @returns {Function} Returns the new function. * @example * * var p = _.wrap(_.escape, function(func, text) { * return '

' + func(text) + '

'; * }); * * p('fred, barney, & pebbles'); * // => '

fred, barney, & pebbles

' */ function wrap(value, wrapper) { return partial(castFunction(wrapper), value); } /*------------------------------------------------------------------------*/ /** * Casts `value` as an array if it's not one. * * @static * @memberOf _ * @since 4.4.0 * @category Lang * @param {*} value The value to inspect. * @returns {Array} Returns the cast array. * @example * * _.castArray(1); * // => [1] * * _.castArray({ 'a': 1 }); * // => [{ 'a': 1 }] * * _.castArray('abc'); * // => ['abc'] * * _.castArray(null); * // => [null] * * _.castArray(undefined); * // => [undefined] * * _.castArray(); * // => [] * * var array = [1, 2, 3]; * console.log(_.castArray(array) === array); * // => true */ function castArray() { if (!arguments.length) { return []; } var value = arguments[0]; return isArray(value) ? value : [value]; } /** * Creates a shallow clone of `value`. * * **Note:** This method is loosely based on the * [structured clone algorithm](https://mdn.io/Structured_clone_algorithm) * and supports cloning arrays, array buffers, booleans, date objects, maps, * numbers, `Object` objects, regexes, sets, strings, symbols, and typed * arrays. The own enumerable properties of `arguments` objects are cloned * as plain objects. An empty object is returned for uncloneable values such * as error objects, functions, DOM nodes, and WeakMaps. * * @static * @memberOf _ * @since 0.1.0 * @category Lang * @param {*} value The value to clone. * @returns {*} Returns the cloned value. * @see _.cloneDeep * @example * * var objects = [{ 'a': 1 }, { 'b': 2 }]; * * var shallow = _.clone(objects); * console.log(shallow[0] === objects[0]); * // => true */ function clone(value) { return baseClone(value, CLONE_SYMBOLS_FLAG); } /** * This method is like `_.clone` except that it accepts `customizer` which * is invoked to produce the cloned value. If `customizer` returns `undefined`, * cloning is handled by the method instead. The `customizer` is invoked with * up to four arguments; (value [, index|key, object, stack]). * * @static * @memberOf _ * @since 4.0.0 * @category Lang * @param {*} value The value to clone. * @param {Function} [customizer] The function to customize cloning. * @returns {*} Returns the cloned value. * @see _.cloneDeepWith * @example * * function customizer(value) { * if (_.isElement(value)) { * return value.cloneNode(false); * } * } * * var el = _.cloneWith(document.body, customizer); * * console.log(el === document.body); * // => false * console.log(el.nodeName); * // => 'BODY' * console.log(el.childNodes.length); * // => 0 */ function cloneWith(value, customizer) { customizer = typeof customizer == 'function' ? customizer : undefined; return baseClone(value, CLONE_SYMBOLS_FLAG, customizer); } /** * This method is like `_.clone` except that it recursively clones `value`. * * @static * @memberOf _ * @since 1.0.0 * @category Lang * @param {*} value The value to recursively clone. * @returns {*} Returns the deep cloned value. * @see _.clone * @example * * var objects = [{ 'a': 1 }, { 'b': 2 }]; * * var deep = _.cloneDeep(objects); * console.log(deep[0] === objects[0]); * // => false */ function cloneDeep(value) { return baseClone(value, CLONE_DEEP_FLAG | CLONE_SYMBOLS_FLAG); } /** * This method is like `_.cloneWith` except that it recursively clones `value`. * * @static * @memberOf _ * @since 4.0.0 * @category Lang * @param {*} value The value to recursively clone. * @param {Function} [customizer] The function to customize cloning. * @returns {*} Returns the deep cloned value. * @see _.cloneWith * @example * * function customizer(value) { * if (_.isElement(value)) { * return value.cloneNode(true); * } * } * * var el = _.cloneDeepWith(document.body, customizer); * * console.log(el === document.body); * // => false * console.log(el.nodeName); * // => 'BODY' * console.log(el.childNodes.length); * // => 20 */ function cloneDeepWith(value, customizer) { customizer = typeof customizer == 'function' ? customizer : undefined; return baseClone(value, CLONE_DEEP_FLAG | CLONE_SYMBOLS_FLAG, customizer); } /** * Checks if `object` conforms to `source` by invoking the predicate * properties of `source` with the corresponding property values of `object`. * * **Note:** This method is equivalent to `_.conforms` when `source` is * partially applied. * * @static * @memberOf _ * @since 4.14.0 * @category Lang * @param {Object} object The object to inspect. * @param {Object} source The object of property predicates to conform to. * @returns {boolean} Returns `true` if `object` conforms, else `false`. * @example * * var object = { 'a': 1, 'b': 2 }; * * _.conformsTo(object, { 'b': function(n) { return n > 1; } }); * // => true * * _.conformsTo(object, { 'b': function(n) { return n > 2; } }); * // => false */ function conformsTo(object, source) { return source == null || baseConformsTo(object, source, keys(source)); } /** * Performs a * [`SameValueZero`](http://ecma-international.org/ecma-262/7.0/#sec-samevaluezero) * comparison between two values to determine if they are equivalent. * * @static * @memberOf _ * @since 4.0.0 * @category Lang * @param {*} value The value to compare. * @param {*} other The other value to compare. * @returns {boolean} Returns `true` if the values are equivalent, else `false`. * @example * * var object = { 'a': 1 }; * var other = { 'a': 1 }; * * _.eq(object, object); * // => true * * _.eq(object, other); * // => false * * _.eq('a', 'a'); * // => true * * _.eq('a', Object('a')); * // => false * * _.eq(NaN, NaN); * // => true */ function eq(value, other) { return value === other || (value !== value && other !== other); } /** * Checks if `value` is greater than `other`. * * @static * @memberOf _ * @since 3.9.0 * @category Lang * @param {*} value The value to compare. * @param {*} other The other value to compare. * @returns {boolean} Returns `true` if `value` is greater than `other`, * else `false`. * @see _.lt * @example * * _.gt(3, 1); * // => true * * _.gt(3, 3); * // => false * * _.gt(1, 3); * // => false */ var gt = createRelationalOperation(baseGt); /** * Checks if `value` is greater than or equal to `other`. * * @static * @memberOf _ * @since 3.9.0 * @category Lang * @param {*} value The value to compare. * @param {*} other The other value to compare. * @returns {boolean} Returns `true` if `value` is greater than or equal to * `other`, else `false`. * @see _.lte * @example * * _.gte(3, 1); * // => true * * _.gte(3, 3); * // => true * * _.gte(1, 3); * // => false */ var gte = createRelationalOperation(function(value, other) { return value >= other; }); /** * Checks if `value` is likely an `arguments` object. * * @static * @memberOf _ * @since 0.1.0 * @category Lang * @param {*} value The value to check. * @returns {boolean} Returns `true` if `value` is an `arguments` object, * else `false`. * @example * * _.isArguments(function() { return arguments; }()); * // => true * * _.isArguments([1, 2, 3]); * // => false */ var isArguments = baseIsArguments(function() { return arguments; }()) ? baseIsArguments : function(value) { return isObjectLike(value) && hasOwnProperty.call(value, 'callee') && !propertyIsEnumerable.call(value, 'callee'); }; /** * Checks if `value` is classified as an `Array` object. * * @static * @memberOf _ * @since 0.1.0 * @category Lang * @param {*} value The value to check. * @returns {boolean} Returns `true` if `value` is an array, else `false`. * @example * * _.isArray([1, 2, 3]); * // => true * * _.isArray(document.body.children); * // => false * * _.isArray('abc'); * // => false * * _.isArray(_.noop); * // => false */ var isArray = Array.isArray; /** * Checks if `value` is classified as an `ArrayBuffer` object. * * @static * @memberOf _ * @since 4.3.0 * @category Lang * @param {*} value The value to check. * @returns {boolean} Returns `true` if `value` is an array buffer, else `false`. * @example * * _.isArrayBuffer(new ArrayBuffer(2)); * // => true * * _.isArrayBuffer(new Array(2)); * // => false */ var isArrayBuffer = nodeIsArrayBuffer ? baseUnary(nodeIsArrayBuffer) : baseIsArrayBuffer; /** * Checks if `value` is array-like. A value is considered array-like if it's * not a function and has a `value.length` that's an integer greater than or * equal to `0` and less than or equal to `Number.MAX_SAFE_INTEGER`. * * @static * @memberOf _ * @since 4.0.0 * @category Lang * @param {*} value The value to check. * @returns {boolean} Returns `true` if `value` is array-like, else `false`. * @example * * _.isArrayLike([1, 2, 3]); * // => true * * _.isArrayLike(document.body.children); * // => true * * _.isArrayLike('abc'); * // => true * * _.isArrayLike(_.noop); * // => false */ function isArrayLike(value) { return value != null && isLength(value.length) && !isFunction(value); } /** * This method is like `_.isArrayLike` except that it also checks if `value` * is an object. * * @static * @memberOf _ * @since 4.0.0 * @category Lang * @param {*} value The value to check. * @returns {boolean} Returns `true` if `value` is an array-like object, * else `false`. * @example * * _.isArrayLikeObject([1, 2, 3]); * // => true * * _.isArrayLikeObject(document.body.children); * // => true * * _.isArrayLikeObject('abc'); * // => false * * _.isArrayLikeObject(_.noop); * // => false */ function isArrayLikeObject(value) { return isObjectLike(value) && isArrayLike(value); } /** * Checks if `value` is classified as a boolean primitive or object. * * @static * @memberOf _ * @since 0.1.0 * @category Lang * @param {*} value The value to check. * @returns {boolean} Returns `true` if `value` is a boolean, else `false`. * @example * * _.isBoolean(false); * // => true * * _.isBoolean(null); * // => false */ function isBoolean(value) { return value === true || value === false || (isObjectLike(value) && baseGetTag(value) == boolTag); } /** * Checks if `value` is a buffer. * * @static * @memberOf _ * @since 4.3.0 * @category Lang * @param {*} value The value to check. * @returns {boolean} Returns `true` if `value` is a buffer, else `false`. * @example * * _.isBuffer(new Buffer(2)); * // => true * * _.isBuffer(new Uint8Array(2)); * // => false */ var isBuffer = nativeIsBuffer || stubFalse; /** * Checks if `value` is classified as a `Date` object. * * @static * @memberOf _ * @since 0.1.0 * @category Lang * @param {*} value The value to check. * @returns {boolean} Returns `true` if `value` is a date object, else `false`. * @example * * _.isDate(new Date); * // => true * * _.isDate('Mon April 23 2012'); * // => false */ var isDate = nodeIsDate ? baseUnary(nodeIsDate) : baseIsDate; /** * Checks if `value` is likely a DOM element. * * @static * @memberOf _ * @since 0.1.0 * @category Lang * @param {*} value The value to check. * @returns {boolean} Returns `true` if `value` is a DOM element, else `false`. * @example * * _.isElement(document.body); * // => true * * _.isElement(''); * // => false */ function isElement(value) { return isObjectLike(value) && value.nodeType === 1 && !isPlainObject(value); } /** * Checks if `value` is an empty object, collection, map, or set. * * Objects are considered empty if they have no own enumerable string keyed * properties. * * Array-like values such as `arguments` objects, arrays, buffers, strings, or * jQuery-like collections are considered empty if they have a `length` of `0`. * Similarly, maps and sets are considered empty if they have a `size` of `0`. * * @static * @memberOf _ * @since 0.1.0 * @category Lang * @param {*} value The value to check. * @returns {boolean} Returns `true` if `value` is empty, else `false`. * @example * * _.isEmpty(null); * // => true * * _.isEmpty(true); * // => true * * _.isEmpty(1); * // => true * * _.isEmpty([1, 2, 3]); * // => false * * _.isEmpty({ 'a': 1 }); * // => false */ function isEmpty(value) { if (value == null) { return true; } if (isArrayLike(value) && (isArray(value) || typeof value == 'string' || typeof value.splice == 'function' || isBuffer(value) || isTypedArray(value) || isArguments(value))) { return !value.length; } var tag = getTag(value); if (tag == mapTag || tag == setTag) { return !value.size; } if (isPrototype(value)) { return !baseKeys(value).length; } for (var key in value) { if (hasOwnProperty.call(value, key)) { return false; } } return true; } /** * Performs a deep comparison between two values to determine if they are * equivalent. * * **Note:** This method supports comparing arrays, array buffers, booleans, * date objects, error objects, maps, numbers, `Object` objects, regexes, * sets, strings, symbols, and typed arrays. `Object` objects are compared * by their own, not inherited, enumerable properties. Functions and DOM * nodes are compared by strict equality, i.e. `===`. * * @static * @memberOf _ * @since 0.1.0 * @category Lang * @param {*} value The value to compare. * @param {*} other The other value to compare. * @returns {boolean} Returns `true` if the values are equivalent, else `false`. * @example * * var object = { 'a': 1 }; * var other = { 'a': 1 }; * * _.isEqual(object, other); * // => true * * object === other; * // => false */ function isEqual(value, other) { return baseIsEqual(value, other); } /** * This method is like `_.isEqual` except that it accepts `customizer` which * is invoked to compare values. If `customizer` returns `undefined`, comparisons * are handled by the method instead. The `customizer` is invoked with up to * six arguments: (objValue, othValue [, index|key, object, other, stack]). * * @static * @memberOf _ * @since 4.0.0 * @category Lang * @param {*} value The value to compare. * @param {*} other The other value to compare. * @param {Function} [customizer] The function to customize comparisons. * @returns {boolean} Returns `true` if the values are equivalent, else `false`. * @example * * function isGreeting(value) { * return /^h(?:i|ello)$/.test(value); * } * * function customizer(objValue, othValue) { * if (isGreeting(objValue) && isGreeting(othValue)) { * return true; * } * } * * var array = ['hello', 'goodbye']; * var other = ['hi', 'goodbye']; * * _.isEqualWith(array, other, customizer); * // => true */ function isEqualWith(value, other, customizer) { customizer = typeof customizer == 'function' ? customizer : undefined; var result = customizer ? customizer(value, other) : undefined; return result === undefined ? baseIsEqual(value, other, undefined, customizer) : !!result; } /** * Checks if `value` is an `Error`, `EvalError`, `RangeError`, `ReferenceError`, * `SyntaxError`, `TypeError`, or `URIError` object. * * @static * @memberOf _ * @since 3.0.0 * @category Lang * @param {*} value The value to check. * @returns {boolean} Returns `true` if `value` is an error object, else `false`. * @example * * _.isError(new Error); * // => true * * _.isError(Error); * // => false */ function isError(value) { if (!isObjectLike(value)) { return false; } var tag = baseGetTag(value); return tag == errorTag || tag == domExcTag || (typeof value.message == 'string' && typeof value.name == 'string' && !isPlainObject(value)); } /** * Checks if `value` is a finite primitive number. * * **Note:** This method is based on * [`Number.isFinite`](https://mdn.io/Number/isFinite). * * @static * @memberOf _ * @since 0.1.0 * @category Lang * @param {*} value The value to check. * @returns {boolean} Returns `true` if `value` is a finite number, else `false`. * @example * * _.isFinite(3); * // => true * * _.isFinite(Number.MIN_VALUE); * // => true * * _.isFinite(Infinity); * // => false * * _.isFinite('3'); * // => false */ function isFinite(value) { return typeof value == 'number' && nativeIsFinite(value); } /** * Checks if `value` is classified as a `Function` object. * * @static * @memberOf _ * @since 0.1.0 * @category Lang * @param {*} value The value to check. * @returns {boolean} Returns `true` if `value` is a function, else `false`. * @example * * _.isFunction(_); * // => true * * _.isFunction(/abc/); * // => false */ function isFunction(value) { if (!isObject(value)) { return false; } // The use of `Object#toString` avoids issues with the `typeof` operator // in Safari 9 which returns 'object' for typed arrays and other constructors. var tag = baseGetTag(value); return tag == funcTag || tag == genTag || tag == asyncTag || tag == proxyTag; } /** * Checks if `value` is an integer. * * **Note:** This method is based on * [`Number.isInteger`](https://mdn.io/Number/isInteger). * * @static * @memberOf _ * @since 4.0.0 * @category Lang * @param {*} value The value to check. * @returns {boolean} Returns `true` if `value` is an integer, else `false`. * @example * * _.isInteger(3); * // => true * * _.isInteger(Number.MIN_VALUE); * // => false * * _.isInteger(Infinity); * // => false * * _.isInteger('3'); * // => false */ function isInteger(value) { return typeof value == 'number' && value == toInteger(value); } /** * Checks if `value` is a valid array-like length. * * **Note:** This method is loosely based on * [`ToLength`](http://ecma-international.org/ecma-262/7.0/#sec-tolength). * * @static * @memberOf _ * @since 4.0.0 * @category Lang * @param {*} value The value to check. * @returns {boolean} Returns `true` if `value` is a valid length, else `false`. * @example * * _.isLength(3); * // => true * * _.isLength(Number.MIN_VALUE); * // => false * * _.isLength(Infinity); * // => false * * _.isLength('3'); * // => false */ function isLength(value) { return typeof value == 'number' && value > -1 && value % 1 == 0 && value <= MAX_SAFE_INTEGER; } /** * Checks if `value` is the * [language type](http://www.ecma-international.org/ecma-262/7.0/#sec-ecmascript-language-types) * of `Object`. (e.g. arrays, functions, objects, regexes, `new Number(0)`, and `new String('')`) * * @static * @memberOf _ * @since 0.1.0 * @category Lang * @param {*} value The value to check. * @returns {boolean} Returns `true` if `value` is an object, else `false`. * @example * * _.isObject({}); * // => true * * _.isObject([1, 2, 3]); * // => true * * _.isObject(_.noop); * // => true * * _.isObject(null); * // => false */ function isObject(value) { var type = typeof value; return value != null && (type == 'object' || type == 'function'); } /** * Checks if `value` is object-like. A value is object-like if it's not `null` * and has a `typeof` result of "object". * * @static * @memberOf _ * @since 4.0.0 * @category Lang * @param {*} value The value to check. * @returns {boolean} Returns `true` if `value` is object-like, else `false`. * @example * * _.isObjectLike({}); * // => true * * _.isObjectLike([1, 2, 3]); * // => true * * _.isObjectLike(_.noop); * // => false * * _.isObjectLike(null); * // => false */ function isObjectLike(value) { return value != null && typeof value == 'object'; } /** * Checks if `value` is classified as a `Map` object. * * @static * @memberOf _ * @since 4.3.0 * @category Lang * @param {*} value The value to check. * @returns {boolean} Returns `true` if `value` is a map, else `false`. * @example * * _.isMap(new Map); * // => true * * _.isMap(new WeakMap); * // => false */ var isMap = nodeIsMap ? baseUnary(nodeIsMap) : baseIsMap; /** * Performs a partial deep comparison between `object` and `source` to * determine if `object` contains equivalent property values. * * **Note:** This method is equivalent to `_.matches` when `source` is * partially applied. * * Partial comparisons will match empty array and empty object `source` * values against any array or object value, respectively. See `_.isEqual` * for a list of supported value comparisons. * * @static * @memberOf _ * @since 3.0.0 * @category Lang * @param {Object} object The object to inspect. * @param {Object} source The object of property values to match. * @returns {boolean} Returns `true` if `object` is a match, else `false`. * @example * * var object = { 'a': 1, 'b': 2 }; * * _.isMatch(object, { 'b': 2 }); * // => true * * _.isMatch(object, { 'b': 1 }); * // => false */ function isMatch(object, source) { return object === source || baseIsMatch(object, source, getMatchData(source)); } /** * This method is like `_.isMatch` except that it accepts `customizer` which * is invoked to compare values. If `customizer` returns `undefined`, comparisons * are handled by the method instead. The `customizer` is invoked with five * arguments: (objValue, srcValue, index|key, object, source). * * @static * @memberOf _ * @since 4.0.0 * @category Lang * @param {Object} object The object to inspect. * @param {Object} source The object of property values to match. * @param {Function} [customizer] The function to customize comparisons. * @returns {boolean} Returns `true` if `object` is a match, else `false`. * @example * * function isGreeting(value) { * return /^h(?:i|ello)$/.test(value); * } * * function customizer(objValue, srcValue) { * if (isGreeting(objValue) && isGreeting(srcValue)) { * return true; * } * } * * var object = { 'greeting': 'hello' }; * var source = { 'greeting': 'hi' }; * * _.isMatchWith(object, source, customizer); * // => true */ function isMatchWith(object, source, customizer) { customizer = typeof customizer == 'function' ? customizer : undefined; return baseIsMatch(object, source, getMatchData(source), customizer); } /** * Checks if `value` is `NaN`. * * **Note:** This method is based on * [`Number.isNaN`](https://mdn.io/Number/isNaN) and is not the same as * global [`isNaN`](https://mdn.io/isNaN) which returns `true` for * `undefined` and other non-number values. * * @static * @memberOf _ * @since 0.1.0 * @category Lang * @param {*} value The value to check. * @returns {boolean} Returns `true` if `value` is `NaN`, else `false`. * @example * * _.isNaN(NaN); * // => true * * _.isNaN(new Number(NaN)); * // => true * * isNaN(undefined); * // => true * * _.isNaN(undefined); * // => false */ function isNaN(value) { // An `NaN` primitive is the only value that is not equal to itself. // Perform the `toStringTag` check first to avoid errors with some // ActiveX objects in IE. return isNumber(value) && value != +value; } /** * Checks if `value` is a pristine native function. * * **Note:** This method can't reliably detect native functions in the presence * of the core-js package because core-js circumvents this kind of detection. * Despite multiple requests, the core-js maintainer has made it clear: any * attempt to fix the detection will be obstructed. As a result, we're left * with little choice but to throw an error. Unfortunately, this also affects * packages, like [babel-polyfill](https://www.npmjs.com/package/babel-polyfill), * which rely on core-js. * * @static * @memberOf _ * @since 3.0.0 * @category Lang * @param {*} value The value to check. * @returns {boolean} Returns `true` if `value` is a native function, * else `false`. * @example * * _.isNative(Array.prototype.push); * // => true * * _.isNative(_); * // => false */ function isNative(value) { if (isMaskable(value)) { throw new Error(CORE_ERROR_TEXT); } return baseIsNative(value); } /** * Checks if `value` is `null`. * * @static * @memberOf _ * @since 0.1.0 * @category Lang * @param {*} value The value to check. * @returns {boolean} Returns `true` if `value` is `null`, else `false`. * @example * * _.isNull(null); * // => true * * _.isNull(void 0); * // => false */ function isNull(value) { return value === null; } /** * Checks if `value` is `null` or `undefined`. * * @static * @memberOf _ * @since 4.0.0 * @category Lang * @param {*} value The value to check. * @returns {boolean} Returns `true` if `value` is nullish, else `false`. * @example * * _.isNil(null); * // => true * * _.isNil(void 0); * // => true * * _.isNil(NaN); * // => false */ function isNil(value) { return value == null; } /** * Checks if `value` is classified as a `Number` primitive or object. * * **Note:** To exclude `Infinity`, `-Infinity`, and `NaN`, which are * classified as numbers, use the `_.isFinite` method. * * @static * @memberOf _ * @since 0.1.0 * @category Lang * @param {*} value The value to check. * @returns {boolean} Returns `true` if `value` is a number, else `false`. * @example * * _.isNumber(3); * // => true * * _.isNumber(Number.MIN_VALUE); * // => true * * _.isNumber(Infinity); * // => true * * _.isNumber('3'); * // => false */ function isNumber(value) { return typeof value == 'number' || (isObjectLike(value) && baseGetTag(value) == numberTag); } /** * Checks if `value` is a plain object, that is, an object created by the * `Object` constructor or one with a `[[Prototype]]` of `null`. * * @static * @memberOf _ * @since 0.8.0 * @category Lang * @param {*} value The value to check. * @returns {boolean} Returns `true` if `value` is a plain object, else `false`. * @example * * function Foo() { * this.a = 1; * } * * _.isPlainObject(new Foo); * // => false * * _.isPlainObject([1, 2, 3]); * // => false * * _.isPlainObject({ 'x': 0, 'y': 0 }); * // => true * * _.isPlainObject(Object.create(null)); * // => true */ function isPlainObject(value) { if (!isObjectLike(value) || baseGetTag(value) != objectTag) { return false; } var proto = getPrototype(value); if (proto === null) { return true; } var Ctor = hasOwnProperty.call(proto, 'constructor') && proto.constructor; return typeof Ctor == 'function' && Ctor instanceof Ctor && funcToString.call(Ctor) == objectCtorString; } /** * Checks if `value` is classified as a `RegExp` object. * * @static * @memberOf _ * @since 0.1.0 * @category Lang * @param {*} value The value to check. * @returns {boolean} Returns `true` if `value` is a regexp, else `false`. * @example * * _.isRegExp(/abc/); * // => true * * _.isRegExp('/abc/'); * // => false */ var isRegExp = nodeIsRegExp ? baseUnary(nodeIsRegExp) : baseIsRegExp; /** * Checks if `value` is a safe integer. An integer is safe if it's an IEEE-754 * double precision number which isn't the result of a rounded unsafe integer. * * **Note:** This method is based on * [`Number.isSafeInteger`](https://mdn.io/Number/isSafeInteger). * * @static * @memberOf _ * @since 4.0.0 * @category Lang * @param {*} value The value to check. * @returns {boolean} Returns `true` if `value` is a safe integer, else `false`. * @example * * _.isSafeInteger(3); * // => true * * _.isSafeInteger(Number.MIN_VALUE); * // => false * * _.isSafeInteger(Infinity); * // => false * * _.isSafeInteger('3'); * // => false */ function isSafeInteger(value) { return isInteger(value) && value >= -MAX_SAFE_INTEGER && value <= MAX_SAFE_INTEGER; } /** * Checks if `value` is classified as a `Set` object. * * @static * @memberOf _ * @since 4.3.0 * @category Lang * @param {*} value The value to check. * @returns {boolean} Returns `true` if `value` is a set, else `false`. * @example * * _.isSet(new Set); * // => true * * _.isSet(new WeakSet); * // => false */ var isSet = nodeIsSet ? baseUnary(nodeIsSet) : baseIsSet; /** * Checks if `value` is classified as a `String` primitive or object. * * @static * @since 0.1.0 * @memberOf _ * @category Lang * @param {*} value The value to check. * @returns {boolean} Returns `true` if `value` is a string, else `false`. * @example * * _.isString('abc'); * // => true * * _.isString(1); * // => false */ function isString(value) { return typeof value == 'string' || (!isArray(value) && isObjectLike(value) && baseGetTag(value) == stringTag); } /** * Checks if `value` is classified as a `Symbol` primitive or object. * * @static * @memberOf _ * @since 4.0.0 * @category Lang * @param {*} value The value to check. * @returns {boolean} Returns `true` if `value` is a symbol, else `false`. * @example * * _.isSymbol(Symbol.iterator); * // => true * * _.isSymbol('abc'); * // => false */ function isSymbol(value) { return typeof value == 'symbol' || (isObjectLike(value) && baseGetTag(value) == symbolTag); } /** * Checks if `value` is classified as a typed array. * * @static * @memberOf _ * @since 3.0.0 * @category Lang * @param {*} value The value to check. * @returns {boolean} Returns `true` if `value` is a typed array, else `false`. * @example * * _.isTypedArray(new Uint8Array); * // => true * * _.isTypedArray([]); * // => false */ var isTypedArray = nodeIsTypedArray ? baseUnary(nodeIsTypedArray) : baseIsTypedArray; /** * Checks if `value` is `undefined`. * * @static * @since 0.1.0 * @memberOf _ * @category Lang * @param {*} value The value to check. * @returns {boolean} Returns `true` if `value` is `undefined`, else `false`. * @example * * _.isUndefined(void 0); * // => true * * _.isUndefined(null); * // => false */ function isUndefined(value) { return value === undefined; } /** * Checks if `value` is classified as a `WeakMap` object. * * @static * @memberOf _ * @since 4.3.0 * @category Lang * @param {*} value The value to check. * @returns {boolean} Returns `true` if `value` is a weak map, else `false`. * @example * * _.isWeakMap(new WeakMap); * // => true * * _.isWeakMap(new Map); * // => false */ function isWeakMap(value) { return isObjectLike(value) && getTag(value) == weakMapTag; } /** * Checks if `value` is classified as a `WeakSet` object. * * @static * @memberOf _ * @since 4.3.0 * @category Lang * @param {*} value The value to check. * @returns {boolean} Returns `true` if `value` is a weak set, else `false`. * @example * * _.isWeakSet(new WeakSet); * // => true * * _.isWeakSet(new Set); * // => false */ function isWeakSet(value) { return isObjectLike(value) && baseGetTag(value) == weakSetTag; } /** * Checks if `value` is less than `other`. * * @static * @memberOf _ * @since 3.9.0 * @category Lang * @param {*} value The value to compare. * @param {*} other The other value to compare. * @returns {boolean} Returns `true` if `value` is less than `other`, * else `false`. * @see _.gt * @example * * _.lt(1, 3); * // => true * * _.lt(3, 3); * // => false * * _.lt(3, 1); * // => false */ var lt = createRelationalOperation(baseLt); /** * Checks if `value` is less than or equal to `other`. * * @static * @memberOf _ * @since 3.9.0 * @category Lang * @param {*} value The value to compare. * @param {*} other The other value to compare. * @returns {boolean} Returns `true` if `value` is less than or equal to * `other`, else `false`. * @see _.gte * @example * * _.lte(1, 3); * // => true * * _.lte(3, 3); * // => true * * _.lte(3, 1); * // => false */ var lte = createRelationalOperation(function(value, other) { return value <= other; }); /** * Converts `value` to an array. * * @static * @since 0.1.0 * @memberOf _ * @category Lang * @param {*} value The value to convert. * @returns {Array} Returns the converted array. * @example * * _.toArray({ 'a': 1, 'b': 2 }); * // => [1, 2] * * _.toArray('abc'); * // => ['a', 'b', 'c'] * * _.toArray(1); * // => [] * * _.toArray(null); * // => [] */ function toArray(value) { if (!value) { return []; } if (isArrayLike(value)) { return isString(value) ? stringToArray(value) : copyArray(value); } if (symIterator && value[symIterator]) { return iteratorToArray(value[symIterator]()); } var tag = getTag(value), func = tag == mapTag ? mapToArray : (tag == setTag ? setToArray : values); return func(value); } /** * Converts `value` to a finite number. * * @static * @memberOf _ * @since 4.12.0 * @category Lang * @param {*} value The value to convert. * @returns {number} Returns the converted number. * @example * * _.toFinite(3.2); * // => 3.2 * * _.toFinite(Number.MIN_VALUE); * // => 5e-324 * * _.toFinite(Infinity); * // => 1.7976931348623157e+308 * * _.toFinite('3.2'); * // => 3.2 */ function toFinite(value) { if (!value) { return value === 0 ? value : 0; } value = toNumber(value); if (value === INFINITY || value === -INFINITY) { var sign = (value < 0 ? -1 : 1); return sign * MAX_INTEGER; } return value === value ? value : 0; } /** * Converts `value` to an integer. * * **Note:** This method is loosely based on * [`ToInteger`](http://www.ecma-international.org/ecma-262/7.0/#sec-tointeger). * * @static * @memberOf _ * @since 4.0.0 * @category Lang * @param {*} value The value to convert. * @returns {number} Returns the converted integer. * @example * * _.toInteger(3.2); * // => 3 * * _.toInteger(Number.MIN_VALUE); * // => 0 * * _.toInteger(Infinity); * // => 1.7976931348623157e+308 * * _.toInteger('3.2'); * // => 3 */ function toInteger(value) { var result = toFinite(value), remainder = result % 1; return result === result ? (remainder ? result - remainder : result) : 0; } /** * Converts `value` to an integer suitable for use as the length of an * array-like object. * * **Note:** This method is based on * [`ToLength`](http://ecma-international.org/ecma-262/7.0/#sec-tolength). * * @static * @memberOf _ * @since 4.0.0 * @category Lang * @param {*} value The value to convert. * @returns {number} Returns the converted integer. * @example * * _.toLength(3.2); * // => 3 * * _.toLength(Number.MIN_VALUE); * // => 0 * * _.toLength(Infinity); * // => 4294967295 * * _.toLength('3.2'); * // => 3 */ function toLength(value) { return value ? baseClamp(toInteger(value), 0, MAX_ARRAY_LENGTH) : 0; } /** * Converts `value` to a number. * * @static * @memberOf _ * @since 4.0.0 * @category Lang * @param {*} value The value to process. * @returns {number} Returns the number. * @example * * _.toNumber(3.2); * // => 3.2 * * _.toNumber(Number.MIN_VALUE); * // => 5e-324 * * _.toNumber(Infinity); * // => Infinity * * _.toNumber('3.2'); * // => 3.2 */ function toNumber(value) { if (typeof value == 'number') { return value; } if (isSymbol(value)) { return NAN; } if (isObject(value)) { var other = typeof value.valueOf == 'function' ? value.valueOf() : value; value = isObject(other) ? (other + '') : other; } if (typeof value != 'string') { return value === 0 ? value : +value; } value = value.replace(reTrim, ''); var isBinary = reIsBinary.test(value); return (isBinary || reIsOctal.test(value)) ? freeParseInt(value.slice(2), isBinary ? 2 : 8) : (reIsBadHex.test(value) ? NAN : +value); } /** * Converts `value` to a plain object flattening inherited enumerable string * keyed properties of `value` to own properties of the plain object. * * @static * @memberOf _ * @since 3.0.0 * @category Lang * @param {*} value The value to convert. * @returns {Object} Returns the converted plain object. * @example * * function Foo() { * this.b = 2; * } * * Foo.prototype.c = 3; * * _.assign({ 'a': 1 }, new Foo); * // => { 'a': 1, 'b': 2 } * * _.assign({ 'a': 1 }, _.toPlainObject(new Foo)); * // => { 'a': 1, 'b': 2, 'c': 3 } */ function toPlainObject(value) { return copyObject(value, keysIn(value)); } /** * Converts `value` to a safe integer. A safe integer can be compared and * represented correctly. * * @static * @memberOf _ * @since 4.0.0 * @category Lang * @param {*} value The value to convert. * @returns {number} Returns the converted integer. * @example * * _.toSafeInteger(3.2); * // => 3 * * _.toSafeInteger(Number.MIN_VALUE); * // => 0 * * _.toSafeInteger(Infinity); * // => 9007199254740991 * * _.toSafeInteger('3.2'); * // => 3 */ function toSafeInteger(value) { return value ? baseClamp(toInteger(value), -MAX_SAFE_INTEGER, MAX_SAFE_INTEGER) : (value === 0 ? value : 0); } /** * Converts `value` to a string. An empty string is returned for `null` * and `undefined` values. The sign of `-0` is preserved. * * @static * @memberOf _ * @since 4.0.0 * @category Lang * @param {*} value The value to convert. * @returns {string} Returns the converted string. * @example * * _.toString(null); * // => '' * * _.toString(-0); * // => '-0' * * _.toString([1, 2, 3]); * // => '1,2,3' */ function toString(value) { return value == null ? '' : baseToString(value); } /*------------------------------------------------------------------------*/ /** * Assigns own enumerable string keyed properties of source objects to the * destination object. Source objects are applied from left to right. * Subsequent sources overwrite property assignments of previous sources. * * **Note:** This method mutates `object` and is loosely based on * [`Object.assign`](https://mdn.io/Object/assign). * * @static * @memberOf _ * @since 0.10.0 * @category Object * @param {Object} object The destination object. * @param {...Object} [sources] The source objects. * @returns {Object} Returns `object`. * @see _.assignIn * @example * * function Foo() { * this.a = 1; * } * * function Bar() { * this.c = 3; * } * * Foo.prototype.b = 2; * Bar.prototype.d = 4; * * _.assign({ 'a': 0 }, new Foo, new Bar); * // => { 'a': 1, 'c': 3 } */ var assign = createAssigner(function(object, source) { if (isPrototype(source) || isArrayLike(source)) { copyObject(source, keys(source), object); return; } for (var key in source) { if (hasOwnProperty.call(source, key)) { assignValue(object, key, source[key]); } } }); /** * This method is like `_.assign` except that it iterates over own and * inherited source properties. * * **Note:** This method mutates `object`. * * @static * @memberOf _ * @since 4.0.0 * @alias extend * @category Object * @param {Object} object The destination object. * @param {...Object} [sources] The source objects. * @returns {Object} Returns `object`. * @see _.assign * @example * * function Foo() { * this.a = 1; * } * * function Bar() { * this.c = 3; * } * * Foo.prototype.b = 2; * Bar.prototype.d = 4; * * _.assignIn({ 'a': 0 }, new Foo, new Bar); * // => { 'a': 1, 'b': 2, 'c': 3, 'd': 4 } */ var assignIn = createAssigner(function(object, source) { copyObject(source, keysIn(source), object); }); /** * This method is like `_.assignIn` except that it accepts `customizer` * which is invoked to produce the assigned values. If `customizer` returns * `undefined`, assignment is handled by the method instead. The `customizer` * is invoked with five arguments: (objValue, srcValue, key, object, source). * * **Note:** This method mutates `object`. * * @static * @memberOf _ * @since 4.0.0 * @alias extendWith * @category Object * @param {Object} object The destination object. * @param {...Object} sources The source objects. * @param {Function} [customizer] The function to customize assigned values. * @returns {Object} Returns `object`. * @see _.assignWith * @example * * function customizer(objValue, srcValue) { * return _.isUndefined(objValue) ? srcValue : objValue; * } * * var defaults = _.partialRight(_.assignInWith, customizer); * * defaults({ 'a': 1 }, { 'b': 2 }, { 'a': 3 }); * // => { 'a': 1, 'b': 2 } */ var assignInWith = createAssigner(function(object, source, srcIndex, customizer) { copyObject(source, keysIn(source), object, customizer); }); /** * This method is like `_.assign` except that it accepts `customizer` * which is invoked to produce the assigned values. If `customizer` returns * `undefined`, assignment is handled by the method instead. The `customizer` * is invoked with five arguments: (objValue, srcValue, key, object, source). * * **Note:** This method mutates `object`. * * @static * @memberOf _ * @since 4.0.0 * @category Object * @param {Object} object The destination object. * @param {...Object} sources The source objects. * @param {Function} [customizer] The function to customize assigned values. * @returns {Object} Returns `object`. * @see _.assignInWith * @example * * function customizer(objValue, srcValue) { * return _.isUndefined(objValue) ? srcValue : objValue; * } * * var defaults = _.partialRight(_.assignWith, customizer); * * defaults({ 'a': 1 }, { 'b': 2 }, { 'a': 3 }); * // => { 'a': 1, 'b': 2 } */ var assignWith = createAssigner(function(object, source, srcIndex, customizer) { copyObject(source, keys(source), object, customizer); }); /** * Creates an array of values corresponding to `paths` of `object`. * * @static * @memberOf _ * @since 1.0.0 * @category Object * @param {Object} object The object to iterate over. * @param {...(string|string[])} [paths] The property paths to pick. * @returns {Array} Returns the picked values. * @example * * var object = { 'a': [{ 'b': { 'c': 3 } }, 4] }; * * _.at(object, ['a[0].b.c', 'a[1]']); * // => [3, 4] */ var at = flatRest(baseAt); /** * Creates an object that inherits from the `prototype` object. If a * `properties` object is given, its own enumerable string keyed properties * are assigned to the created object. * * @static * @memberOf _ * @since 2.3.0 * @category Object * @param {Object} prototype The object to inherit from. * @param {Object} [properties] The properties to assign to the object. * @returns {Object} Returns the new object. * @example * * function Shape() { * this.x = 0; * this.y = 0; * } * * function Circle() { * Shape.call(this); * } * * Circle.prototype = _.create(Shape.prototype, { * 'constructor': Circle * }); * * var circle = new Circle; * circle instanceof Circle; * // => true * * circle instanceof Shape; * // => true */ function create(prototype, properties) { var result = baseCreate(prototype); return properties == null ? result : baseAssign(result, properties); } /** * Assigns own and inherited enumerable string keyed properties of source * objects to the destination object for all destination properties that * resolve to `undefined`. Source objects are applied from left to right. * Once a property is set, additional values of the same property are ignored. * * **Note:** This method mutates `object`. * * @static * @since 0.1.0 * @memberOf _ * @category Object * @param {Object} object The destination object. * @param {...Object} [sources] The source objects. * @returns {Object} Returns `object`. * @see _.defaultsDeep * @example * * _.defaults({ 'a': 1 }, { 'b': 2 }, { 'a': 3 }); * // => { 'a': 1, 'b': 2 } */ var defaults = baseRest(function(object, sources) { object = Object(object); var index = -1; var length = sources.length; var guard = length > 2 ? sources[2] : undefined; if (guard && isIterateeCall(sources[0], sources[1], guard)) { length = 1; } while (++index < length) { var source = sources[index]; var props = keysIn(source); var propsIndex = -1; var propsLength = props.length; while (++propsIndex < propsLength) { var key = props[propsIndex]; var value = object[key]; if (value === undefined || (eq(value, objectProto[key]) && !hasOwnProperty.call(object, key))) { object[key] = source[key]; } } } return object; }); /** * This method is like `_.defaults` except that it recursively assigns * default properties. * * **Note:** This method mutates `object`. * * @static * @memberOf _ * @since 3.10.0 * @category Object * @param {Object} object The destination object. * @param {...Object} [sources] The source objects. * @returns {Object} Returns `object`. * @see _.defaults * @example * * _.defaultsDeep({ 'a': { 'b': 2 } }, { 'a': { 'b': 1, 'c': 3 } }); * // => { 'a': { 'b': 2, 'c': 3 } } */ var defaultsDeep = baseRest(function(args) { args.push(undefined, customDefaultsMerge); return apply(mergeWith, undefined, args); }); /** * This method is like `_.find` except that it returns the key of the first * element `predicate` returns truthy for instead of the element itself. * * @static * @memberOf _ * @since 1.1.0 * @category Object * @param {Object} object The object to inspect. * @param {Function} [predicate=_.identity] The function invoked per iteration. * @returns {string|undefined} Returns the key of the matched element, * else `undefined`. * @example * * var users = { * 'barney': { 'age': 36, 'active': true }, * 'fred': { 'age': 40, 'active': false }, * 'pebbles': { 'age': 1, 'active': true } * }; * * _.findKey(users, function(o) { return o.age < 40; }); * // => 'barney' (iteration order is not guaranteed) * * // The `_.matches` iteratee shorthand. * _.findKey(users, { 'age': 1, 'active': true }); * // => 'pebbles' * * // The `_.matchesProperty` iteratee shorthand. * _.findKey(users, ['active', false]); * // => 'fred' * * // The `_.property` iteratee shorthand. * _.findKey(users, 'active'); * // => 'barney' */ function findKey(object, predicate) { return baseFindKey(object, getIteratee(predicate, 3), baseForOwn); } /** * This method is like `_.findKey` except that it iterates over elements of * a collection in the opposite order. * * @static * @memberOf _ * @since 2.0.0 * @category Object * @param {Object} object The object to inspect. * @param {Function} [predicate=_.identity] The function invoked per iteration. * @returns {string|undefined} Returns the key of the matched element, * else `undefined`. * @example * * var users = { * 'barney': { 'age': 36, 'active': true }, * 'fred': { 'age': 40, 'active': false }, * 'pebbles': { 'age': 1, 'active': true } * }; * * _.findLastKey(users, function(o) { return o.age < 40; }); * // => returns 'pebbles' assuming `_.findKey` returns 'barney' * * // The `_.matches` iteratee shorthand. * _.findLastKey(users, { 'age': 36, 'active': true }); * // => 'barney' * * // The `_.matchesProperty` iteratee shorthand. * _.findLastKey(users, ['active', false]); * // => 'fred' * * // The `_.property` iteratee shorthand. * _.findLastKey(users, 'active'); * // => 'pebbles' */ function findLastKey(object, predicate) { return baseFindKey(object, getIteratee(predicate, 3), baseForOwnRight); } /** * Iterates over own and inherited enumerable string keyed properties of an * object and invokes `iteratee` for each property. The iteratee is invoked * with three arguments: (value, key, object). Iteratee functions may exit * iteration early by explicitly returning `false`. * * @static * @memberOf _ * @since 0.3.0 * @category Object * @param {Object} object The object to iterate over. * @param {Function} [iteratee=_.identity] The function invoked per iteration. * @returns {Object} Returns `object`. * @see _.forInRight * @example * * function Foo() { * this.a = 1; * this.b = 2; * } * * Foo.prototype.c = 3; * * _.forIn(new Foo, function(value, key) { * console.log(key); * }); * // => Logs 'a', 'b', then 'c' (iteration order is not guaranteed). */ function forIn(object, iteratee) { return object == null ? object : baseFor(object, getIteratee(iteratee, 3), keysIn); } /** * This method is like `_.forIn` except that it iterates over properties of * `object` in the opposite order. * * @static * @memberOf _ * @since 2.0.0 * @category Object * @param {Object} object The object to iterate over. * @param {Function} [iteratee=_.identity] The function invoked per iteration. * @returns {Object} Returns `object`. * @see _.forIn * @example * * function Foo() { * this.a = 1; * this.b = 2; * } * * Foo.prototype.c = 3; * * _.forInRight(new Foo, function(value, key) { * console.log(key); * }); * // => Logs 'c', 'b', then 'a' assuming `_.forIn` logs 'a', 'b', then 'c'. */ function forInRight(object, iteratee) { return object == null ? object : baseForRight(object, getIteratee(iteratee, 3), keysIn); } /** * Iterates over own enumerable string keyed properties of an object and * invokes `iteratee` for each property. The iteratee is invoked with three * arguments: (value, key, object). Iteratee functions may exit iteration * early by explicitly returning `false`. * * @static * @memberOf _ * @since 0.3.0 * @category Object * @param {Object} object The object to iterate over. * @param {Function} [iteratee=_.identity] The function invoked per iteration. * @returns {Object} Returns `object`. * @see _.forOwnRight * @example * * function Foo() { * this.a = 1; * this.b = 2; * } * * Foo.prototype.c = 3; * * _.forOwn(new Foo, function(value, key) { * console.log(key); * }); * // => Logs 'a' then 'b' (iteration order is not guaranteed). */ function forOwn(object, iteratee) { return object && baseForOwn(object, getIteratee(iteratee, 3)); } /** * This method is like `_.forOwn` except that it iterates over properties of * `object` in the opposite order. * * @static * @memberOf _ * @since 2.0.0 * @category Object * @param {Object} object The object to iterate over. * @param {Function} [iteratee=_.identity] The function invoked per iteration. * @returns {Object} Returns `object`. * @see _.forOwn * @example * * function Foo() { * this.a = 1; * this.b = 2; * } * * Foo.prototype.c = 3; * * _.forOwnRight(new Foo, function(value, key) { * console.log(key); * }); * // => Logs 'b' then 'a' assuming `_.forOwn` logs 'a' then 'b'. */ function forOwnRight(object, iteratee) { return object && baseForOwnRight(object, getIteratee(iteratee, 3)); } /** * Creates an array of function property names from own enumerable properties * of `object`. * * @static * @since 0.1.0 * @memberOf _ * @category Object * @param {Object} object The object to inspect. * @returns {Array} Returns the function names. * @see _.functionsIn * @example * * function Foo() { * this.a = _.constant('a'); * this.b = _.constant('b'); * } * * Foo.prototype.c = _.constant('c'); * * _.functions(new Foo); * // => ['a', 'b'] */ function functions(object) { return object == null ? [] : baseFunctions(object, keys(object)); } /** * Creates an array of function property names from own and inherited * enumerable properties of `object`. * * @static * @memberOf _ * @since 4.0.0 * @category Object * @param {Object} object The object to inspect. * @returns {Array} Returns the function names. * @see _.functions * @example * * function Foo() { * this.a = _.constant('a'); * this.b = _.constant('b'); * } * * Foo.prototype.c = _.constant('c'); * * _.functionsIn(new Foo); * // => ['a', 'b', 'c'] */ function functionsIn(object) { return object == null ? [] : baseFunctions(object, keysIn(object)); } /** * Gets the value at `path` of `object`. If the resolved value is * `undefined`, the `defaultValue` is returned in its place. * * @static * @memberOf _ * @since 3.7.0 * @category Object * @param {Object} object The object to query. * @param {Array|string} path The path of the property to get. * @param {*} [defaultValue] The value returned for `undefined` resolved values. * @returns {*} Returns the resolved value. * @example * * var object = { 'a': [{ 'b': { 'c': 3 } }] }; * * _.get(object, 'a[0].b.c'); * // => 3 * * _.get(object, ['a', '0', 'b', 'c']); * // => 3 * * _.get(object, 'a.b.c', 'default'); * // => 'default' */ function get(object, path, defaultValue) { var result = object == null ? undefined : baseGet(object, path); return result === undefined ? defaultValue : result; } /** * Checks if `path` is a direct property of `object`. * * @static * @since 0.1.0 * @memberOf _ * @category Object * @param {Object} object The object to query. * @param {Array|string} path The path to check. * @returns {boolean} Returns `true` if `path` exists, else `false`. * @example * * var object = { 'a': { 'b': 2 } }; * var other = _.create({ 'a': _.create({ 'b': 2 }) }); * * _.has(object, 'a'); * // => true * * _.has(object, 'a.b'); * // => true * * _.has(object, ['a', 'b']); * // => true * * _.has(other, 'a'); * // => false */ function has(object, path) { return object != null && hasPath(object, path, baseHas); } /** * Checks if `path` is a direct or inherited property of `object`. * * @static * @memberOf _ * @since 4.0.0 * @category Object * @param {Object} object The object to query. * @param {Array|string} path The path to check. * @returns {boolean} Returns `true` if `path` exists, else `false`. * @example * * var object = _.create({ 'a': _.create({ 'b': 2 }) }); * * _.hasIn(object, 'a'); * // => true * * _.hasIn(object, 'a.b'); * // => true * * _.hasIn(object, ['a', 'b']); * // => true * * _.hasIn(object, 'b'); * // => false */ function hasIn(object, path) { return object != null && hasPath(object, path, baseHasIn); } /** * Creates an object composed of the inverted keys and values of `object`. * If `object` contains duplicate values, subsequent values overwrite * property assignments of previous values. * * @static * @memberOf _ * @since 0.7.0 * @category Object * @param {Object} object The object to invert. * @returns {Object} Returns the new inverted object. * @example * * var object = { 'a': 1, 'b': 2, 'c': 1 }; * * _.invert(object); * // => { '1': 'c', '2': 'b' } */ var invert = createInverter(function(result, value, key) { if (value != null && typeof value.toString != 'function') { value = nativeObjectToString.call(value); } result[value] = key; }, constant(identity)); /** * This method is like `_.invert` except that the inverted object is generated * from the results of running each element of `object` thru `iteratee`. The * corresponding inverted value of each inverted key is an array of keys * responsible for generating the inverted value. The iteratee is invoked * with one argument: (value). * * @static * @memberOf _ * @since 4.1.0 * @category Object * @param {Object} object The object to invert. * @param {Function} [iteratee=_.identity] The iteratee invoked per element. * @returns {Object} Returns the new inverted object. * @example * * var object = { 'a': 1, 'b': 2, 'c': 1 }; * * _.invertBy(object); * // => { '1': ['a', 'c'], '2': ['b'] } * * _.invertBy(object, function(value) { * return 'group' + value; * }); * // => { 'group1': ['a', 'c'], 'group2': ['b'] } */ var invertBy = createInverter(function(result, value, key) { if (value != null && typeof value.toString != 'function') { value = nativeObjectToString.call(value); } if (hasOwnProperty.call(result, value)) { result[value].push(key); } else { result[value] = [key]; } }, getIteratee); /** * Invokes the method at `path` of `object`. * * @static * @memberOf _ * @since 4.0.0 * @category Object * @param {Object} object The object to query. * @param {Array|string} path The path of the method to invoke. * @param {...*} [args] The arguments to invoke the method with. * @returns {*} Returns the result of the invoked method. * @example * * var object = { 'a': [{ 'b': { 'c': [1, 2, 3, 4] } }] }; * * _.invoke(object, 'a[0].b.c.slice', 1, 3); * // => [2, 3] */ var invoke = baseRest(baseInvoke); /** * Creates an array of the own enumerable property names of `object`. * * **Note:** Non-object values are coerced to objects. See the * [ES spec](http://ecma-international.org/ecma-262/7.0/#sec-object.keys) * for more details. * * @static * @since 0.1.0 * @memberOf _ * @category Object * @param {Object} object The object to query. * @returns {Array} Returns the array of property names. * @example * * function Foo() { * this.a = 1; * this.b = 2; * } * * Foo.prototype.c = 3; * * _.keys(new Foo); * // => ['a', 'b'] (iteration order is not guaranteed) * * _.keys('hi'); * // => ['0', '1'] */ function keys(object) { return isArrayLike(object) ? arrayLikeKeys(object) : baseKeys(object); } /** * Creates an array of the own and inherited enumerable property names of `object`. * * **Note:** Non-object values are coerced to objects. * * @static * @memberOf _ * @since 3.0.0 * @category Object * @param {Object} object The object to query. * @returns {Array} Returns the array of property names. * @example * * function Foo() { * this.a = 1; * this.b = 2; * } * * Foo.prototype.c = 3; * * _.keysIn(new Foo); * // => ['a', 'b', 'c'] (iteration order is not guaranteed) */ function keysIn(object) { return isArrayLike(object) ? arrayLikeKeys(object, true) : baseKeysIn(object); } /** * The opposite of `_.mapValues`; this method creates an object with the * same values as `object` and keys generated by running each own enumerable * string keyed property of `object` thru `iteratee`. The iteratee is invoked * with three arguments: (value, key, object). * * @static * @memberOf _ * @since 3.8.0 * @category Object * @param {Object} object The object to iterate over. * @param {Function} [iteratee=_.identity] The function invoked per iteration. * @returns {Object} Returns the new mapped object. * @see _.mapValues * @example * * _.mapKeys({ 'a': 1, 'b': 2 }, function(value, key) { * return key + value; * }); * // => { 'a1': 1, 'b2': 2 } */ function mapKeys(object, iteratee) { var result = {}; iteratee = getIteratee(iteratee, 3); baseForOwn(object, function(value, key, object) { baseAssignValue(result, iteratee(value, key, object), value); }); return result; } /** * Creates an object with the same keys as `object` and values generated * by running each own enumerable string keyed property of `object` thru * `iteratee`. The iteratee is invoked with three arguments: * (value, key, object). * * @static * @memberOf _ * @since 2.4.0 * @category Object * @param {Object} object The object to iterate over. * @param {Function} [iteratee=_.identity] The function invoked per iteration. * @returns {Object} Returns the new mapped object. * @see _.mapKeys * @example * * var users = { * 'fred': { 'user': 'fred', 'age': 40 }, * 'pebbles': { 'user': 'pebbles', 'age': 1 } * }; * * _.mapValues(users, function(o) { return o.age; }); * // => { 'fred': 40, 'pebbles': 1 } (iteration order is not guaranteed) * * // The `_.property` iteratee shorthand. * _.mapValues(users, 'age'); * // => { 'fred': 40, 'pebbles': 1 } (iteration order is not guaranteed) */ function mapValues(object, iteratee) { var result = {}; iteratee = getIteratee(iteratee, 3); baseForOwn(object, function(value, key, object) { baseAssignValue(result, key, iteratee(value, key, object)); }); return result; } /** * This method is like `_.assign` except that it recursively merges own and * inherited enumerable string keyed properties of source objects into the * destination object. Source properties that resolve to `undefined` are * skipped if a destination value exists. Array and plain object properties * are merged recursively. Other objects and value types are overridden by * assignment. Source objects are applied from left to right. Subsequent * sources overwrite property assignments of previous sources. * * **Note:** This method mutates `object`. * * @static * @memberOf _ * @since 0.5.0 * @category Object * @param {Object} object The destination object. * @param {...Object} [sources] The source objects. * @returns {Object} Returns `object`. * @example * * var object = { * 'a': [{ 'b': 2 }, { 'd': 4 }] * }; * * var other = { * 'a': [{ 'c': 3 }, { 'e': 5 }] * }; * * _.merge(object, other); * // => { 'a': [{ 'b': 2, 'c': 3 }, { 'd': 4, 'e': 5 }] } */ var merge = createAssigner(function(object, source, srcIndex) { baseMerge(object, source, srcIndex); }); /** * This method is like `_.merge` except that it accepts `customizer` which * is invoked to produce the merged values of the destination and source * properties. If `customizer` returns `undefined`, merging is handled by the * method instead. The `customizer` is invoked with six arguments: * (objValue, srcValue, key, object, source, stack). * * **Note:** This method mutates `object`. * * @static * @memberOf _ * @since 4.0.0 * @category Object * @param {Object} object The destination object. * @param {...Object} sources The source objects. * @param {Function} customizer The function to customize assigned values. * @returns {Object} Returns `object`. * @example * * function customizer(objValue, srcValue) { * if (_.isArray(objValue)) { * return objValue.concat(srcValue); * } * } * * var object = { 'a': [1], 'b': [2] }; * var other = { 'a': [3], 'b': [4] }; * * _.mergeWith(object, other, customizer); * // => { 'a': [1, 3], 'b': [2, 4] } */ var mergeWith = createAssigner(function(object, source, srcIndex, customizer) { baseMerge(object, source, srcIndex, customizer); }); /** * The opposite of `_.pick`; this method creates an object composed of the * own and inherited enumerable property paths of `object` that are not omitted. * * **Note:** This method is considerably slower than `_.pick`. * * @static * @since 0.1.0 * @memberOf _ * @category Object * @param {Object} object The source object. * @param {...(string|string[])} [paths] The property paths to omit. * @returns {Object} Returns the new object. * @example * * var object = { 'a': 1, 'b': '2', 'c': 3 }; * * _.omit(object, ['a', 'c']); * // => { 'b': '2' } */ var omit = flatRest(function(object, paths) { var result = {}; if (object == null) { return result; } var isDeep = false; paths = arrayMap(paths, function(path) { path = castPath(path, object); isDeep || (isDeep = path.length > 1); return path; }); copyObject(object, getAllKeysIn(object), result); if (isDeep) { result = baseClone(result, CLONE_DEEP_FLAG | CLONE_FLAT_FLAG | CLONE_SYMBOLS_FLAG, customOmitClone); } var length = paths.length; while (length--) { baseUnset(result, paths[length]); } return result; }); /** * The opposite of `_.pickBy`; this method creates an object composed of * the own and inherited enumerable string keyed properties of `object` that * `predicate` doesn't return truthy for. The predicate is invoked with two * arguments: (value, key). * * @static * @memberOf _ * @since 4.0.0 * @category Object * @param {Object} object The source object. * @param {Function} [predicate=_.identity] The function invoked per property. * @returns {Object} Returns the new object. * @example * * var object = { 'a': 1, 'b': '2', 'c': 3 }; * * _.omitBy(object, _.isNumber); * // => { 'b': '2' } */ function omitBy(object, predicate) { return pickBy(object, negate(getIteratee(predicate))); } /** * Creates an object composed of the picked `object` properties. * * @static * @since 0.1.0 * @memberOf _ * @category Object * @param {Object} object The source object. * @param {...(string|string[])} [paths] The property paths to pick. * @returns {Object} Returns the new object. * @example * * var object = { 'a': 1, 'b': '2', 'c': 3 }; * * _.pick(object, ['a', 'c']); * // => { 'a': 1, 'c': 3 } */ var pick = flatRest(function(object, paths) { return object == null ? {} : basePick(object, paths); }); /** * Creates an object composed of the `object` properties `predicate` returns * truthy for. The predicate is invoked with two arguments: (value, key). * * @static * @memberOf _ * @since 4.0.0 * @category Object * @param {Object} object The source object. * @param {Function} [predicate=_.identity] The function invoked per property. * @returns {Object} Returns the new object. * @example * * var object = { 'a': 1, 'b': '2', 'c': 3 }; * * _.pickBy(object, _.isNumber); * // => { 'a': 1, 'c': 3 } */ function pickBy(object, predicate) { if (object == null) { return {}; } var props = arrayMap(getAllKeysIn(object), function(prop) { return [prop]; }); predicate = getIteratee(predicate); return basePickBy(object, props, function(value, path) { return predicate(value, path[0]); }); } /** * This method is like `_.get` except that if the resolved value is a * function it's invoked with the `this` binding of its parent object and * its result is returned. * * @static * @since 0.1.0 * @memberOf _ * @category Object * @param {Object} object The object to query. * @param {Array|string} path The path of the property to resolve. * @param {*} [defaultValue] The value returned for `undefined` resolved values. * @returns {*} Returns the resolved value. * @example * * var object = { 'a': [{ 'b': { 'c1': 3, 'c2': _.constant(4) } }] }; * * _.result(object, 'a[0].b.c1'); * // => 3 * * _.result(object, 'a[0].b.c2'); * // => 4 * * _.result(object, 'a[0].b.c3', 'default'); * // => 'default' * * _.result(object, 'a[0].b.c3', _.constant('default')); * // => 'default' */ function result(object, path, defaultValue) { path = castPath(path, object); var index = -1, length = path.length; // Ensure the loop is entered when path is empty. if (!length) { length = 1; object = undefined; } while (++index < length) { var value = object == null ? undefined : object[toKey(path[index])]; if (value === undefined) { index = length; value = defaultValue; } object = isFunction(value) ? value.call(object) : value; } return object; } /** * Sets the value at `path` of `object`. If a portion of `path` doesn't exist, * it's created. Arrays are created for missing index properties while objects * are created for all other missing properties. Use `_.setWith` to customize * `path` creation. * * **Note:** This method mutates `object`. * * @static * @memberOf _ * @since 3.7.0 * @category Object * @param {Object} object The object to modify. * @param {Array|string} path The path of the property to set. * @param {*} value The value to set. * @returns {Object} Returns `object`. * @example * * var object = { 'a': [{ 'b': { 'c': 3 } }] }; * * _.set(object, 'a[0].b.c', 4); * console.log(object.a[0].b.c); * // => 4 * * _.set(object, ['x', '0', 'y', 'z'], 5); * console.log(object.x[0].y.z); * // => 5 */ function set(object, path, value) { return object == null ? object : baseSet(object, path, value); } /** * This method is like `_.set` except that it accepts `customizer` which is * invoked to produce the objects of `path`. If `customizer` returns `undefined` * path creation is handled by the method instead. The `customizer` is invoked * with three arguments: (nsValue, key, nsObject). * * **Note:** This method mutates `object`. * * @static * @memberOf _ * @since 4.0.0 * @category Object * @param {Object} object The object to modify. * @param {Array|string} path The path of the property to set. * @param {*} value The value to set. * @param {Function} [customizer] The function to customize assigned values. * @returns {Object} Returns `object`. * @example * * var object = {}; * * _.setWith(object, '[0][1]', 'a', Object); * // => { '0': { '1': 'a' } } */ function setWith(object, path, value, customizer) { customizer = typeof customizer == 'function' ? customizer : undefined; return object == null ? object : baseSet(object, path, value, customizer); } /** * Creates an array of own enumerable string keyed-value pairs for `object` * which can be consumed by `_.fromPairs`. If `object` is a map or set, its * entries are returned. * * @static * @memberOf _ * @since 4.0.0 * @alias entries * @category Object * @param {Object} object The object to query. * @returns {Array} Returns the key-value pairs. * @example * * function Foo() { * this.a = 1; * this.b = 2; * } * * Foo.prototype.c = 3; * * _.toPairs(new Foo); * // => [['a', 1], ['b', 2]] (iteration order is not guaranteed) */ var toPairs = createToPairs(keys); /** * Creates an array of own and inherited enumerable string keyed-value pairs * for `object` which can be consumed by `_.fromPairs`. If `object` is a map * or set, its entries are returned. * * @static * @memberOf _ * @since 4.0.0 * @alias entriesIn * @category Object * @param {Object} object The object to query. * @returns {Array} Returns the key-value pairs. * @example * * function Foo() { * this.a = 1; * this.b = 2; * } * * Foo.prototype.c = 3; * * _.toPairsIn(new Foo); * // => [['a', 1], ['b', 2], ['c', 3]] (iteration order is not guaranteed) */ var toPairsIn = createToPairs(keysIn); /** * An alternative to `_.reduce`; this method transforms `object` to a new * `accumulator` object which is the result of running each of its own * enumerable string keyed properties thru `iteratee`, with each invocation * potentially mutating the `accumulator` object. If `accumulator` is not * provided, a new object with the same `[[Prototype]]` will be used. The * iteratee is invoked with four arguments: (accumulator, value, key, object). * Iteratee functions may exit iteration early by explicitly returning `false`. * * @static * @memberOf _ * @since 1.3.0 * @category Object * @param {Object} object The object to iterate over. * @param {Function} [iteratee=_.identity] The function invoked per iteration. * @param {*} [accumulator] The custom accumulator value. * @returns {*} Returns the accumulated value. * @example * * _.transform([2, 3, 4], function(result, n) { * result.push(n *= n); * return n % 2 == 0; * }, []); * // => [4, 9] * * _.transform({ 'a': 1, 'b': 2, 'c': 1 }, function(result, value, key) { * (result[value] || (result[value] = [])).push(key); * }, {}); * // => { '1': ['a', 'c'], '2': ['b'] } */ function transform(object, iteratee, accumulator) { var isArr = isArray(object), isArrLike = isArr || isBuffer(object) || isTypedArray(object); iteratee = getIteratee(iteratee, 4); if (accumulator == null) { var Ctor = object && object.constructor; if (isArrLike) { accumulator = isArr ? new Ctor : []; } else if (isObject(object)) { accumulator = isFunction(Ctor) ? baseCreate(getPrototype(object)) : {}; } else { accumulator = {}; } } (isArrLike ? arrayEach : baseForOwn)(object, function(value, index, object) { return iteratee(accumulator, value, index, object); }); return accumulator; } /** * Removes the property at `path` of `object`. * * **Note:** This method mutates `object`. * * @static * @memberOf _ * @since 4.0.0 * @category Object * @param {Object} object The object to modify. * @param {Array|string} path The path of the property to unset. * @returns {boolean} Returns `true` if the property is deleted, else `false`. * @example * * var object = { 'a': [{ 'b': { 'c': 7 } }] }; * _.unset(object, 'a[0].b.c'); * // => true * * console.log(object); * // => { 'a': [{ 'b': {} }] }; * * _.unset(object, ['a', '0', 'b', 'c']); * // => true * * console.log(object); * // => { 'a': [{ 'b': {} }] }; */ function unset(object, path) { return object == null ? true : baseUnset(object, path); } /** * This method is like `_.set` except that accepts `updater` to produce the * value to set. Use `_.updateWith` to customize `path` creation. The `updater` * is invoked with one argument: (value). * * **Note:** This method mutates `object`. * * @static * @memberOf _ * @since 4.6.0 * @category Object * @param {Object} object The object to modify. * @param {Array|string} path The path of the property to set. * @param {Function} updater The function to produce the updated value. * @returns {Object} Returns `object`. * @example * * var object = { 'a': [{ 'b': { 'c': 3 } }] }; * * _.update(object, 'a[0].b.c', function(n) { return n * n; }); * console.log(object.a[0].b.c); * // => 9 * * _.update(object, 'x[0].y.z', function(n) { return n ? n + 1 : 0; }); * console.log(object.x[0].y.z); * // => 0 */ function update(object, path, updater) { return object == null ? object : baseUpdate(object, path, castFunction(updater)); } /** * This method is like `_.update` except that it accepts `customizer` which is * invoked to produce the objects of `path`. If `customizer` returns `undefined` * path creation is handled by the method instead. The `customizer` is invoked * with three arguments: (nsValue, key, nsObject). * * **Note:** This method mutates `object`. * * @static * @memberOf _ * @since 4.6.0 * @category Object * @param {Object} object The object to modify. * @param {Array|string} path The path of the property to set. * @param {Function} updater The function to produce the updated value. * @param {Function} [customizer] The function to customize assigned values. * @returns {Object} Returns `object`. * @example * * var object = {}; * * _.updateWith(object, '[0][1]', _.constant('a'), Object); * // => { '0': { '1': 'a' } } */ function updateWith(object, path, updater, customizer) { customizer = typeof customizer == 'function' ? customizer : undefined; return object == null ? object : baseUpdate(object, path, castFunction(updater), customizer); } /** * Creates an array of the own enumerable string keyed property values of `object`. * * **Note:** Non-object values are coerced to objects. * * @static * @since 0.1.0 * @memberOf _ * @category Object * @param {Object} object The object to query. * @returns {Array} Returns the array of property values. * @example * * function Foo() { * this.a = 1; * this.b = 2; * } * * Foo.prototype.c = 3; * * _.values(new Foo); * // => [1, 2] (iteration order is not guaranteed) * * _.values('hi'); * // => ['h', 'i'] */ function values(object) { return object == null ? [] : baseValues(object, keys(object)); } /** * Creates an array of the own and inherited enumerable string keyed property * values of `object`. * * **Note:** Non-object values are coerced to objects. * * @static * @memberOf _ * @since 3.0.0 * @category Object * @param {Object} object The object to query. * @returns {Array} Returns the array of property values. * @example * * function Foo() { * this.a = 1; * this.b = 2; * } * * Foo.prototype.c = 3; * * _.valuesIn(new Foo); * // => [1, 2, 3] (iteration order is not guaranteed) */ function valuesIn(object) { return object == null ? [] : baseValues(object, keysIn(object)); } /*------------------------------------------------------------------------*/ /** * Clamps `number` within the inclusive `lower` and `upper` bounds. * * @static * @memberOf _ * @since 4.0.0 * @category Number * @param {number} number The number to clamp. * @param {number} [lower] The lower bound. * @param {number} upper The upper bound. * @returns {number} Returns the clamped number. * @example * * _.clamp(-10, -5, 5); * // => -5 * * _.clamp(10, -5, 5); * // => 5 */ function clamp(number, lower, upper) { if (upper === undefined) { upper = lower; lower = undefined; } if (upper !== undefined) { upper = toNumber(upper); upper = upper === upper ? upper : 0; } if (lower !== undefined) { lower = toNumber(lower); lower = lower === lower ? lower : 0; } return baseClamp(toNumber(number), lower, upper); } /** * Checks if `n` is between `start` and up to, but not including, `end`. If * `end` is not specified, it's set to `start` with `start` then set to `0`. * If `start` is greater than `end` the params are swapped to support * negative ranges. * * @static * @memberOf _ * @since 3.3.0 * @category Number * @param {number} number The number to check. * @param {number} [start=0] The start of the range. * @param {number} end The end of the range. * @returns {boolean} Returns `true` if `number` is in the range, else `false`. * @see _.range, _.rangeRight * @example * * _.inRange(3, 2, 4); * // => true * * _.inRange(4, 8); * // => true * * _.inRange(4, 2); * // => false * * _.inRange(2, 2); * // => false * * _.inRange(1.2, 2); * // => true * * _.inRange(5.2, 4); * // => false * * _.inRange(-3, -2, -6); * // => true */ function inRange(number, start, end) { start = toFinite(start); if (end === undefined) { end = start; start = 0; } else { end = toFinite(end); } number = toNumber(number); return baseInRange(number, start, end); } /** * Produces a random number between the inclusive `lower` and `upper` bounds. * If only one argument is provided a number between `0` and the given number * is returned. If `floating` is `true`, or either `lower` or `upper` are * floats, a floating-point number is returned instead of an integer. * * **Note:** JavaScript follows the IEEE-754 standard for resolving * floating-point values which can produce unexpected results. * * @static * @memberOf _ * @since 0.7.0 * @category Number * @param {number} [lower=0] The lower bound. * @param {number} [upper=1] The upper bound. * @param {boolean} [floating] Specify returning a floating-point number. * @returns {number} Returns the random number. * @example * * _.random(0, 5); * // => an integer between 0 and 5 * * _.random(5); * // => also an integer between 0 and 5 * * _.random(5, true); * // => a floating-point number between 0 and 5 * * _.random(1.2, 5.2); * // => a floating-point number between 1.2 and 5.2 */ function random(lower, upper, floating) { if (floating && typeof floating != 'boolean' && isIterateeCall(lower, upper, floating)) { upper = floating = undefined; } if (floating === undefined) { if (typeof upper == 'boolean') { floating = upper; upper = undefined; } else if (typeof lower == 'boolean') { floating = lower; lower = undefined; } } if (lower === undefined && upper === undefined) { lower = 0; upper = 1; } else { lower = toFinite(lower); if (upper === undefined) { upper = lower; lower = 0; } else { upper = toFinite(upper); } } if (lower > upper) { var temp = lower; lower = upper; upper = temp; } if (floating || lower % 1 || upper % 1) { var rand = nativeRandom(); return nativeMin(lower + (rand * (upper - lower + freeParseFloat('1e-' + ((rand + '').length - 1)))), upper); } return baseRandom(lower, upper); } /*------------------------------------------------------------------------*/ /** * Converts `string` to [camel case](https://en.wikipedia.org/wiki/CamelCase). * * @static * @memberOf _ * @since 3.0.0 * @category String * @param {string} [string=''] The string to convert. * @returns {string} Returns the camel cased string. * @example * * _.camelCase('Foo Bar'); * // => 'fooBar' * * _.camelCase('--foo-bar--'); * // => 'fooBar' * * _.camelCase('__FOO_BAR__'); * // => 'fooBar' */ var camelCase = createCompounder(function(result, word, index) { word = word.toLowerCase(); return result + (index ? capitalize(word) : word); }); /** * Converts the first character of `string` to upper case and the remaining * to lower case. * * @static * @memberOf _ * @since 3.0.0 * @category String * @param {string} [string=''] The string to capitalize. * @returns {string} Returns the capitalized string. * @example * * _.capitalize('FRED'); * // => 'Fred' */ function capitalize(string) { return upperFirst(toString(string).toLowerCase()); } /** * Deburrs `string` by converting * [Latin-1 Supplement](https://en.wikipedia.org/wiki/Latin-1_Supplement_(Unicode_block)#Character_table) * and [Latin Extended-A](https://en.wikipedia.org/wiki/Latin_Extended-A) * letters to basic Latin letters and removing * [combining diacritical marks](https://en.wikipedia.org/wiki/Combining_Diacritical_Marks). * * @static * @memberOf _ * @since 3.0.0 * @category String * @param {string} [string=''] The string to deburr. * @returns {string} Returns the deburred string. * @example * * _.deburr('déjà vu'); * // => 'deja vu' */ function deburr(string) { string = toString(string); return string && string.replace(reLatin, deburrLetter).replace(reComboMark, ''); } /** * Checks if `string` ends with the given target string. * * @static * @memberOf _ * @since 3.0.0 * @category String * @param {string} [string=''] The string to inspect. * @param {string} [target] The string to search for. * @param {number} [position=string.length] The position to search up to. * @returns {boolean} Returns `true` if `string` ends with `target`, * else `false`. * @example * * _.endsWith('abc', 'c'); * // => true * * _.endsWith('abc', 'b'); * // => false * * _.endsWith('abc', 'b', 2); * // => true */ function endsWith(string, target, position) { string = toString(string); target = baseToString(target); var length = string.length; position = position === undefined ? length : baseClamp(toInteger(position), 0, length); var end = position; position -= target.length; return position >= 0 && string.slice(position, end) == target; } /** * Converts the characters "&", "<", ">", '"', and "'" in `string` to their * corresponding HTML entities. * * **Note:** No other characters are escaped. To escape additional * characters use a third-party library like [_he_](https://mths.be/he). * * Though the ">" character is escaped for symmetry, characters like * ">" and "/" don't need escaping in HTML and have no special meaning * unless they're part of a tag or unquoted attribute value. See * [Mathias Bynens's article](https://mathiasbynens.be/notes/ambiguous-ampersands) * (under "semi-related fun fact") for more details. * * When working with HTML you should always * [quote attribute values](http://wonko.com/post/html-escaping) to reduce * XSS vectors. * * @static * @since 0.1.0 * @memberOf _ * @category String * @param {string} [string=''] The string to escape. * @returns {string} Returns the escaped string. * @example * * _.escape('fred, barney, & pebbles'); * // => 'fred, barney, & pebbles' */ function escape(string) { string = toString(string); return (string && reHasUnescapedHtml.test(string)) ? string.replace(reUnescapedHtml, escapeHtmlChar) : string; } /** * Escapes the `RegExp` special characters "^", "$", "\", ".", "*", "+", * "?", "(", ")", "[", "]", "{", "}", and "|" in `string`. * * @static * @memberOf _ * @since 3.0.0 * @category String * @param {string} [string=''] The string to escape. * @returns {string} Returns the escaped string. * @example * * _.escapeRegExp('[lodash](https://lodash.com/)'); * // => '\[lodash\]\(https://lodash\.com/\)' */ function escapeRegExp(string) { string = toString(string); return (string && reHasRegExpChar.test(string)) ? string.replace(reRegExpChar, '\\$&') : string; } /** * Converts `string` to * [kebab case](https://en.wikipedia.org/wiki/Letter_case#Special_case_styles). * * @static * @memberOf _ * @since 3.0.0 * @category String * @param {string} [string=''] The string to convert. * @returns {string} Returns the kebab cased string. * @example * * _.kebabCase('Foo Bar'); * // => 'foo-bar' * * _.kebabCase('fooBar'); * // => 'foo-bar' * * _.kebabCase('__FOO_BAR__'); * // => 'foo-bar' */ var kebabCase = createCompounder(function(result, word, index) { return result + (index ? '-' : '') + word.toLowerCase(); }); /** * Converts `string`, as space separated words, to lower case. * * @static * @memberOf _ * @since 4.0.0 * @category String * @param {string} [string=''] The string to convert. * @returns {string} Returns the lower cased string. * @example * * _.lowerCase('--Foo-Bar--'); * // => 'foo bar' * * _.lowerCase('fooBar'); * // => 'foo bar' * * _.lowerCase('__FOO_BAR__'); * // => 'foo bar' */ var lowerCase = createCompounder(function(result, word, index) { return result + (index ? ' ' : '') + word.toLowerCase(); }); /** * Converts the first character of `string` to lower case. * * @static * @memberOf _ * @since 4.0.0 * @category String * @param {string} [string=''] The string to convert. * @returns {string} Returns the converted string. * @example * * _.lowerFirst('Fred'); * // => 'fred' * * _.lowerFirst('FRED'); * // => 'fRED' */ var lowerFirst = createCaseFirst('toLowerCase'); /** * Pads `string` on the left and right sides if it's shorter than `length`. * Padding characters are truncated if they can't be evenly divided by `length`. * * @static * @memberOf _ * @since 3.0.0 * @category String * @param {string} [string=''] The string to pad. * @param {number} [length=0] The padding length. * @param {string} [chars=' '] The string used as padding. * @returns {string} Returns the padded string. * @example * * _.pad('abc', 8); * // => ' abc ' * * _.pad('abc', 8, '_-'); * // => '_-abc_-_' * * _.pad('abc', 3); * // => 'abc' */ function pad(string, length, chars) { string = toString(string); length = toInteger(length); var strLength = length ? stringSize(string) : 0; if (!length || strLength >= length) { return string; } var mid = (length - strLength) / 2; return ( createPadding(nativeFloor(mid), chars) + string + createPadding(nativeCeil(mid), chars) ); } /** * Pads `string` on the right side if it's shorter than `length`. Padding * characters are truncated if they exceed `length`. * * @static * @memberOf _ * @since 4.0.0 * @category String * @param {string} [string=''] The string to pad. * @param {number} [length=0] The padding length. * @param {string} [chars=' '] The string used as padding. * @returns {string} Returns the padded string. * @example * * _.padEnd('abc', 6); * // => 'abc ' * * _.padEnd('abc', 6, '_-'); * // => 'abc_-_' * * _.padEnd('abc', 3); * // => 'abc' */ function padEnd(string, length, chars) { string = toString(string); length = toInteger(length); var strLength = length ? stringSize(string) : 0; return (length && strLength < length) ? (string + createPadding(length - strLength, chars)) : string; } /** * Pads `string` on the left side if it's shorter than `length`. Padding * characters are truncated if they exceed `length`. * * @static * @memberOf _ * @since 4.0.0 * @category String * @param {string} [string=''] The string to pad. * @param {number} [length=0] The padding length. * @param {string} [chars=' '] The string used as padding. * @returns {string} Returns the padded string. * @example * * _.padStart('abc', 6); * // => ' abc' * * _.padStart('abc', 6, '_-'); * // => '_-_abc' * * _.padStart('abc', 3); * // => 'abc' */ function padStart(string, length, chars) { string = toString(string); length = toInteger(length); var strLength = length ? stringSize(string) : 0; return (length && strLength < length) ? (createPadding(length - strLength, chars) + string) : string; } /** * Converts `string` to an integer of the specified radix. If `radix` is * `undefined` or `0`, a `radix` of `10` is used unless `value` is a * hexadecimal, in which case a `radix` of `16` is used. * * **Note:** This method aligns with the * [ES5 implementation](https://es5.github.io/#x15.1.2.2) of `parseInt`. * * @static * @memberOf _ * @since 1.1.0 * @category String * @param {string} string The string to convert. * @param {number} [radix=10] The radix to interpret `value` by. * @param- {Object} [guard] Enables use as an iteratee for methods like `_.map`. * @returns {number} Returns the converted integer. * @example * * _.parseInt('08'); * // => 8 * * _.map(['6', '08', '10'], _.parseInt); * // => [6, 8, 10] */ function parseInt(string, radix, guard) { if (guard || radix == null) { radix = 0; } else if (radix) { radix = +radix; } return nativeParseInt(toString(string).replace(reTrimStart, ''), radix || 0); } /** * Repeats the given string `n` times. * * @static * @memberOf _ * @since 3.0.0 * @category String * @param {string} [string=''] The string to repeat. * @param {number} [n=1] The number of times to repeat the string. * @param- {Object} [guard] Enables use as an iteratee for methods like `_.map`. * @returns {string} Returns the repeated string. * @example * * _.repeat('*', 3); * // => '***' * * _.repeat('abc', 2); * // => 'abcabc' * * _.repeat('abc', 0); * // => '' */ function repeat(string, n, guard) { if ((guard ? isIterateeCall(string, n, guard) : n === undefined)) { n = 1; } else { n = toInteger(n); } return baseRepeat(toString(string), n); } /** * Replaces matches for `pattern` in `string` with `replacement`. * * **Note:** This method is based on * [`String#replace`](https://mdn.io/String/replace). * * @static * @memberOf _ * @since 4.0.0 * @category String * @param {string} [string=''] The string to modify. * @param {RegExp|string} pattern The pattern to replace. * @param {Function|string} replacement The match replacement. * @returns {string} Returns the modified string. * @example * * _.replace('Hi Fred', 'Fred', 'Barney'); * // => 'Hi Barney' */ function replace() { var args = arguments, string = toString(args[0]); return args.length < 3 ? string : string.replace(args[1], args[2]); } /** * Converts `string` to * [snake case](https://en.wikipedia.org/wiki/Snake_case). * * @static * @memberOf _ * @since 3.0.0 * @category String * @param {string} [string=''] The string to convert. * @returns {string} Returns the snake cased string. * @example * * _.snakeCase('Foo Bar'); * // => 'foo_bar' * * _.snakeCase('fooBar'); * // => 'foo_bar' * * _.snakeCase('--FOO-BAR--'); * // => 'foo_bar' */ var snakeCase = createCompounder(function(result, word, index) { return result + (index ? '_' : '') + word.toLowerCase(); }); /** * Splits `string` by `separator`. * * **Note:** This method is based on * [`String#split`](https://mdn.io/String/split). * * @static * @memberOf _ * @since 4.0.0 * @category String * @param {string} [string=''] The string to split. * @param {RegExp|string} separator The separator pattern to split by. * @param {number} [limit] The length to truncate results to. * @returns {Array} Returns the string segments. * @example * * _.split('a-b-c', '-', 2); * // => ['a', 'b'] */ function split(string, separator, limit) { if (limit && typeof limit != 'number' && isIterateeCall(string, separator, limit)) { separator = limit = undefined; } limit = limit === undefined ? MAX_ARRAY_LENGTH : limit >>> 0; if (!limit) { return []; } string = toString(string); if (string && ( typeof separator == 'string' || (separator != null && !isRegExp(separator)) )) { separator = baseToString(separator); if (!separator && hasUnicode(string)) { return castSlice(stringToArray(string), 0, limit); } } return string.split(separator, limit); } /** * Converts `string` to * [start case](https://en.wikipedia.org/wiki/Letter_case#Stylistic_or_specialised_usage). * * @static * @memberOf _ * @since 3.1.0 * @category String * @param {string} [string=''] The string to convert. * @returns {string} Returns the start cased string. * @example * * _.startCase('--foo-bar--'); * // => 'Foo Bar' * * _.startCase('fooBar'); * // => 'Foo Bar' * * _.startCase('__FOO_BAR__'); * // => 'FOO BAR' */ var startCase = createCompounder(function(result, word, index) { return result + (index ? ' ' : '') + upperFirst(word); }); /** * Checks if `string` starts with the given target string. * * @static * @memberOf _ * @since 3.0.0 * @category String * @param {string} [string=''] The string to inspect. * @param {string} [target] The string to search for. * @param {number} [position=0] The position to search from. * @returns {boolean} Returns `true` if `string` starts with `target`, * else `false`. * @example * * _.startsWith('abc', 'a'); * // => true * * _.startsWith('abc', 'b'); * // => false * * _.startsWith('abc', 'b', 1); * // => true */ function startsWith(string, target, position) { string = toString(string); position = position == null ? 0 : baseClamp(toInteger(position), 0, string.length); target = baseToString(target); return string.slice(position, position + target.length) == target; } /** * Creates a compiled template function that can interpolate data properties * in "interpolate" delimiters, HTML-escape interpolated data properties in * "escape" delimiters, and execute JavaScript in "evaluate" delimiters. Data * properties may be accessed as free variables in the template. If a setting * object is given, it takes precedence over `_.templateSettings` values. * * **Note:** In the development build `_.template` utilizes * [sourceURLs](http://www.html5rocks.com/en/tutorials/developertools/sourcemaps/#toc-sourceurl) * for easier debugging. * * For more information on precompiling templates see * [lodash's custom builds documentation](https://lodash.com/custom-builds). * * For more information on Chrome extension sandboxes see * [Chrome's extensions documentation](https://developer.chrome.com/extensions/sandboxingEval). * * @static * @since 0.1.0 * @memberOf _ * @category String * @param {string} [string=''] The template string. * @param {Object} [options={}] The options object. * @param {RegExp} [options.escape=_.templateSettings.escape] * The HTML "escape" delimiter. * @param {RegExp} [options.evaluate=_.templateSettings.evaluate] * The "evaluate" delimiter. * @param {Object} [options.imports=_.templateSettings.imports] * An object to import into the template as free variables. * @param {RegExp} [options.interpolate=_.templateSettings.interpolate] * The "interpolate" delimiter. * @param {string} [options.sourceURL='lodash.templateSources[n]'] * The sourceURL of the compiled template. * @param {string} [options.variable='obj'] * The data object variable name. * @param- {Object} [guard] Enables use as an iteratee for methods like `_.map`. * @returns {Function} Returns the compiled template function. * @example * * // Use the "interpolate" delimiter to create a compiled template. * var compiled = _.template('hello <%= user %>!'); * compiled({ 'user': 'fred' }); * // => 'hello fred!' * * // Use the HTML "escape" delimiter to escape data property values. * var compiled = _.template('<%- value %>'); * compiled({ 'value': ' {{ end }} {{ define "page/body_end" }} {{ end }} ================================================ FILE: web/html/component/aClientTable.html ================================================ {{define "component/aClientTable"}} {{end}} ================================================ FILE: web/html/component/aCustomStatistic.html ================================================ {{define "component/customStatistic"}} {{end}} {{define "component/aCustomStatistic"}} {{end}} ================================================ FILE: web/html/component/aPersianDatepicker.html ================================================ {{define "component/persianDatepickerTemplate"}} {{end}} {{define "component/aPersianDatepicker"}} {{end}} ================================================ FILE: web/html/component/aSettingListItem.html ================================================ {{define "component/settingListItem"}} {{end}} {{define "component/aSettingListItem"}} {{end}} ================================================ FILE: web/html/component/aSidebar.html ================================================ {{define "component/sidebar/content"}} {{end}} {{define "component/aSidebar"}} {{end}} ================================================ FILE: web/html/component/aTableSortable.html ================================================ {{define "component/sortableTableTrigger"}} {{end}} {{define "component/aTableSortable"}} {{end}} ================================================ FILE: web/html/component/aThemeSwitch.html ================================================ {{define "component/themeSwitchTemplate"}} {{end}} {{define "component/themeSwitchTemplateLogin"}} {{end}} {{define "component/aThemeSwitch"}} {{end}} ================================================ FILE: web/html/form/client.html ================================================ {{define "form/client"}} [[ key ]] {{ i18n "none" }} [[ key ]] [[ SizeFormatter.sizeFormat(clientStats.up) ]] / [[ SizeFormatter.sizeFormat(clientStats.down) ]] ([[ SizeFormatter.sizeFormat(clientStats.up + clientStats.down) ]]) Expired {{end}} ================================================ FILE: web/html/form/inbound.html ================================================ {{define "form/inbound"}} [[ p ]] {{ i18n "pages.inbounds.periodicTrafficReset.never" }} {{ i18n "pages.inbounds.periodicTrafficReset.daily" }} {{ i18n "pages.inbounds.periodicTrafficReset.weekly" }} {{ i18n "pages.inbounds.periodicTrafficReset.monthly" }} {{template "form/sniffing"}} {{end}} ================================================ FILE: web/html/form/outbound.html ================================================ {{define "form/outbound"}} [[ y ]] {{end}} ================================================ FILE: web/html/form/protocol/dokodemo.html ================================================ {{define "form/tunnel"}} + - TCP,UDP TCP UDP {{end}} ================================================ FILE: web/html/form/protocol/http.html ================================================ {{define "form/http"}}
{{ i18n "username" }} {{ i18n "password" }}
{{end}} ================================================ FILE: web/html/form/protocol/shadowsocks.html ================================================ {{define "form/shadowsocks"}} [[ method_name ]] TCP,UDP TCP UDP {{end}} ================================================ FILE: web/html/form/protocol/socks.html ================================================ {{define "form/mixed"}} {{end}} ================================================ FILE: web/html/form/protocol/trojan.html ================================================ {{define "form/trojan"}} {{template "form/client"}}
{{ i18n "pages.inbounds.email" }} Password
[[ client.email ]] [[ client.password ]]
{{end}} ================================================ FILE: web/html/form/protocol/tun.html ================================================ {{define "form/tun"}} {{end}} ================================================ FILE: web/html/form/protocol/vless.html ================================================ {{define "form/vless"}} {{template "form/client"}}
{{ i18n "pages.inbounds.email" }} ID
[[ client.email ]] [[ client.id ]]
{{end}} ================================================ FILE: web/html/form/protocol/vmess.html ================================================ {{define "form/vmess"}} {{template "form/client"}}
{{ i18n "pages.inbounds.email" }} ID {{ i18n "security" }}
[[ client.email ]] [[ client.id ]] [[ client.security ]]
{{end}} ================================================ FILE: web/html/form/protocol/wireguard.html ================================================ {{define "form/wireguard"}} Peer [[ index + 1 ]] {{end}} ================================================ FILE: web/html/form/reality_settings.html ================================================ {{define "form/realitySettings"}} {{end}} ================================================ FILE: web/html/form/sniffing.html ================================================ {{define "form/sniffing"}} {{ i18n "enabled" }} {{end}} ================================================ FILE: web/html/form/stream/external_proxy.html ================================================ {{define "form/externalProxy"}} {{end}} ================================================ FILE: web/html/form/stream/stream_finalmask.html ================================================ {{define "form/streamFinalMask"}} {{end}} ================================================ FILE: web/html/form/stream/stream_grpc.html ================================================ {{define "form/streamGRPC"}} {{end}} ================================================ FILE: web/html/form/stream/stream_httpupgrade.html ================================================ {{define "form/streamHTTPUpgrade"}} {{end}} ================================================ FILE: web/html/form/stream/stream_kcp.html ================================================ {{define "form/streamKCP"}} {{end}} ================================================ FILE: web/html/form/stream/stream_settings.html ================================================ {{define "form/streamSettings"}} TCP (RAW) mKCP WebSocket gRPC HTTPUpgrade XHTTP {{end}} ================================================ FILE: web/html/form/stream/stream_sockopt.html ================================================ {{define "form/streamSockopt"}} {{end}} ================================================ FILE: web/html/form/stream/stream_tcp.html ================================================ {{define "form/streamTCP"}} {{ i18n "pages.inbounds.stream.general.request" }} {{ i18n "pages.inbounds.stream.general.response" }} {{end}} ================================================ FILE: web/html/form/stream/stream_ws.html ================================================ {{define "form/streamWS"}} {{end}} ================================================ FILE: web/html/form/stream/stream_xhttp.html ================================================ {{define "form/streamXHTTP"}} [[ key ]] Default (POST) POST PUT GET (packet-up only) Default (path) path header cookie query Default (path) path header cookie query Default (body) body header query {{end}} ================================================ FILE: web/html/form/tls_settings.html ================================================ {{define "form/tlsSettings"}} {{ i18n "none" }} Reality TLS {{end}} ================================================ FILE: web/html/inbounds.html ================================================ {{ template "page/head_start" .}} {{ template "page/head_end" .}} {{ template "page/body_start" .}}
{{ i18n "none" }} {{ i18n "disabled" }} {{ i18n "depleted" }} {{ i18n "depletingSoon" }} {{ i18n "online" }}
{{template "page/body_scripts" .}} {{template "component/aSidebar" .}} {{template "component/aThemeSwitch" .}} {{template "component/aCustomStatistic" .}} {{template "component/aPersianDatepicker" .}} {{template "modals/inboundModal"}} {{template "modals/promptModal"}} {{template "modals/qrcodeModal"}} {{template "modals/textModal"}} {{template "modals/inboundInfoModal"}} {{template "modals/clientsModal"}} {{template "modals/clientsBulkModal"}} {{ template "page/body_end" .}} ================================================ FILE: web/html/index.html ================================================ {{ template "page/head_start" .}} {{ template "page/head_end" .}} {{ template "page/body_start" .}} [[ version ]] [[ file ]]
{{ i18n "pages.index.geofilesUpdateAll" }}
10 20 50 100 500 Debug Info Notice Warning Error SysLog
10 20 50 100 500 Direct Blocked Proxy
Timeframe: [[ cpuHistoryModal.bucket ]] sec per point (total [[ cpuHistoryLong.length ]] points)
{{template "page/body_scripts" .}} {{template "component/aSidebar" .}} {{template "component/aThemeSwitch" .}} {{template "component/aCustomStatistic" .}} {{template "modals/textModal"}} {{ template "page/body_end" .}} ================================================ FILE: web/html/login.html ================================================ {{ template "page/head_start" .}} {{ template "page/head_end" .}} {{ template "page/body_start" .}}
{{template "page/body_scripts" .}} {{template "component/aThemeSwitch" .}} {{ template "page/body_end" .}} ================================================ FILE: web/html/modals/client_bulk_modal.html ================================================ {{define "modals/clientsBulkModal"}} Random Random+Prefix Random+Prefix+Num Random+Prefix+Num+Postfix Prefix+Num+Postfix [[ key ]] {{ i18n "none" }} [[ key ]] {{end}} ================================================ FILE: web/html/modals/client_modal.html ================================================ {{define "modals/clientsModal"}} {{template "form/client"}} {{end}} ================================================ FILE: web/html/modals/dns_presets_modal.html ================================================ {{define "modals/dnsPresetsModal"}}
[[ dns.family ? '{{ i18n "pages.xray.dns.dnsPresetFamily" }}' : 'DNS' ]] [[ dns.name ]] {{ i18n "install" }}
{{end}} ================================================ FILE: web/html/modals/inbound_info_modal.html ================================================ {{define "modals/inboundInfoModal"}}
{{ i18n "protocol" }} [[ dbInbound.protocol ]]
{{ i18n "pages.inbounds.address" }} [[ dbInbound.address ]]
{{ i18n "pages.inbounds.port" }} [[ dbInbound.port ]]
{{ i18n "encryption" }} [[ inbound.settings.method ]]
{{ i18n "password" }} [[ inbound.settings.password ]]
{{ i18n "pages.inbounds.network" }} [[ inbound.settings.network ]]
{{end}} ================================================ FILE: web/html/modals/inbound_modal.html ================================================ {{define "modals/inboundModal"}} {{template "form/inbound"}} {{end}} ================================================ FILE: web/html/modals/prompt_modal.html ================================================ {{define "modals/promptModal"}} {{end}} ================================================ FILE: web/html/modals/qrcode_modal.html ================================================ {{define "modals/qrcodeModal"}} {{end}} ================================================ FILE: web/html/modals/text_modal.html ================================================ {{define "modals/textModal"}} {{end}} ================================================ FILE: web/html/modals/two_factor_modal.html ================================================ {{define "modals/twoFactorModal"}} {{end}} ================================================ FILE: web/html/modals/warp_modal.html ================================================ {{define "modals/warpModal"}} {{end}} ================================================ FILE: web/html/modals/xray_balancer_modal.html ================================================ {{define "modals/balancerModal"}} Random Round Robin Least Load Least Ping [[ tag ]] [[ tag ]] {{end}} ================================================ FILE: web/html/modals/xray_dns_modal.html ================================================ {{define "modals/dnsModal"}} [[ l ]] {{end}} ================================================ FILE: web/html/modals/xray_fakedns_modal.html ================================================ {{define "modals/fakednsModal"}} {{end}} ================================================ FILE: web/html/modals/xray_outbound_modal.html ================================================ {{define "modals/outModal"}} {{template "form/outbound"}} {{end}} ================================================ FILE: web/html/modals/xray_reverse_modal.html ================================================ {{define "modals/reverseModal"}} [[ x ]] {{end}} ================================================ FILE: web/html/modals/xray_rule_modal.html ================================================ {{define "modals/ruleModal"}} [[ x ]] [[ x ]] [[ tag ]] [[ tag ]] [[ tag ]] {{end}} ================================================ FILE: web/html/settings/panel/general.html ================================================ {{define "settings/panel/general"}} {{end}} ================================================ FILE: web/html/settings/panel/security.html ================================================ {{define "settings/panel/security"}} {{ i18n "confirm" }} {{end}} ================================================ FILE: web/html/settings/panel/subscription/general.html ================================================ {{define "settings/panel/subscription/general"}} {{ i18n "pages.xray.basicTemplate"}} {{ i18n "pages.xray.advancedTemplate"}} (Happ) {{end}} ================================================ FILE: web/html/settings/panel/subscription/json.html ================================================ {{define "settings/panel/subscription/json"}} Remove Add Noise {{end}} ================================================ FILE: web/html/settings/panel/subscription/subpage.html ================================================ {{ template "page/head_start" .}} {{ template "page/head_end" .}} {{ template "page/body_start" .}} {{ i18n "pages.settings.subSettings"}} {{ i18n "pages.settings.subSettings"}} Json [[ app.sId ]] [[ app.download ]] [[ app.upload ]] [[ app.used ]] [[ app.total ]] [[ app.remained ]]
[[ linkName(link, idx) ]]

Android V2Box V2RayNG Sing-box V2RayTun NPV Tunnel Happ iOS Shadowrocket V2Box Streisand V2RayTun NPV Tunnel Happ
{{template "component/aThemeSwitch" .}} {{ template "page/body_end" .}} ================================================ FILE: web/html/settings/panel/telegram.html ================================================ {{define "settings/panel/telegram"}} {{end}} ================================================ FILE: web/html/settings/xray/advanced.html ================================================ {{define "settings/xray/advanced"}} {{ i18n "pages.xray.completeTemplate"}} {{ i18n "pages.xray.Inbounds" }} {{ i18n "pages.xray.Outbounds" }} {{ i18n "pages.xray.Routings" }} {{end}} ================================================ FILE: web/html/settings/xray/balancers.html ================================================ {{define "settings/xray/balancers"}} {{end}} ================================================ FILE: web/html/settings/xray/basics.html ================================================ {{define "settings/xray/basics"}} {{ i18n "pages.settings.resetDefaultConfig" }} {{end}} ================================================ FILE: web/html/settings/xray/dns.html ================================================ {{define "settings/xray/dns"}} {{end}} ================================================ FILE: web/html/settings/xray/outbounds.html ================================================ {{define "settings/xray/outbounds"}} {{ i18n "pages.xray.outbound.addOutbound" }} WARP {{end}} ================================================ FILE: web/html/settings/xray/reverse.html ================================================ {{define "settings/xray/reverse"}} {{end}} ================================================ FILE: web/html/settings/xray/routing.html ================================================ {{define "settings/xray/routing"}} {{ i18n "pages.xray.rules.add" }} {{end}} ================================================ FILE: web/html/settings.html ================================================ {{ template "page/head_start" .}} {{ template "page/head_end" .}} {{ template "page/body_start" .}} {{template "page/body_scripts" .}} {{template "component/aSidebar" .}} {{template "component/aThemeSwitch" .}} {{template "component/aSettingListItem" .}} {{template "modals/twoFactorModal"}} {{ template "page/body_end" .}} ================================================ FILE: web/html/xray.html ================================================ {{ template "page/head_start" .}} {{ template "page/head_end" .}} {{ template "page/body_start" .}} {{ i18n "pages.xray.save" }} {{ i18n "pages.xray.restart" }} {{ i18n "pages.index.xrayErrorPopoverTitle" }} {{ template "settings/xray/basics" . }} {{ template "settings/xray/routing" . }} {{ template "settings/xray/outbounds" . }} {{ template "settings/xray/reverse" . }} {{ template "settings/xray/balancers" . }} {{ template "settings/xray/dns" . }} {{ template "settings/xray/advanced" . }} {{template "page/body_scripts" .}} {{template "component/aSidebar" .}} {{template "component/aThemeSwitch" .}} {{template "component/aTableSortable" .}} {{template "component/aSettingListItem" .}} {{template "modals/ruleModal"}} {{template "modals/outModal"}} {{template "modals/reverseModal"}} {{template "modals/balancerModal"}} {{template "modals/dnsModal"}} {{template "modals/dnsPresetsModal"}} {{template "modals/fakednsModal"}} {{template "modals/warpModal"}} {{ template "page/body_end" .}} ================================================ FILE: web/job/check_client_ip_job.go ================================================ package job import ( "bufio" "encoding/json" "io" "log" "os" "os/exec" "regexp" "runtime" "sort" "time" "github.com/mhsanaei/3x-ui/v2/database" "github.com/mhsanaei/3x-ui/v2/database/model" "github.com/mhsanaei/3x-ui/v2/logger" "github.com/mhsanaei/3x-ui/v2/xray" ) // IPWithTimestamp tracks an IP address with its last seen timestamp type IPWithTimestamp struct { IP string `json:"ip"` Timestamp int64 `json:"timestamp"` } // CheckClientIpJob monitors client IP addresses from access logs and manages IP blocking based on configured limits. type CheckClientIpJob struct { lastClear int64 disAllowedIps []string } var job *CheckClientIpJob // NewCheckClientIpJob creates a new client IP monitoring job instance. func NewCheckClientIpJob() *CheckClientIpJob { job = new(CheckClientIpJob) return job } func (j *CheckClientIpJob) Run() { if j.lastClear == 0 { j.lastClear = time.Now().Unix() } shouldClearAccessLog := false iplimitActive := j.hasLimitIp() f2bInstalled := j.checkFail2BanInstalled() isAccessLogAvailable := j.checkAccessLogAvailable(iplimitActive) if isAccessLogAvailable { if runtime.GOOS == "windows" { if iplimitActive { shouldClearAccessLog = j.processLogFile() } } else { if iplimitActive { if f2bInstalled { shouldClearAccessLog = j.processLogFile() } else { if !f2bInstalled { logger.Warning("[LimitIP] Fail2Ban is not installed, Please install Fail2Ban from the x-ui bash menu.") } } } } } if shouldClearAccessLog || (isAccessLogAvailable && time.Now().Unix()-j.lastClear > 3600) { j.clearAccessLog() } } func (j *CheckClientIpJob) clearAccessLog() { logAccessP, err := os.OpenFile(xray.GetAccessPersistentLogPath(), os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0o644) j.checkError(err) defer logAccessP.Close() accessLogPath, err := xray.GetAccessLogPath() j.checkError(err) file, err := os.Open(accessLogPath) j.checkError(err) defer file.Close() _, err = io.Copy(logAccessP, file) j.checkError(err) err = os.Truncate(accessLogPath, 0) j.checkError(err) j.lastClear = time.Now().Unix() } func (j *CheckClientIpJob) hasLimitIp() bool { db := database.GetDB() var inbounds []*model.Inbound err := db.Model(model.Inbound{}).Find(&inbounds).Error if err != nil { return false } for _, inbound := range inbounds { if inbound.Settings == "" { continue } settings := map[string][]model.Client{} json.Unmarshal([]byte(inbound.Settings), &settings) clients := settings["clients"] for _, client := range clients { limitIp := client.LimitIP if limitIp > 0 { return true } } } return false } func (j *CheckClientIpJob) processLogFile() bool { ipRegex := regexp.MustCompile(`from (?:tcp:|udp:)?\[?([0-9a-fA-F\.:]+)\]?:\d+ accepted`) emailRegex := regexp.MustCompile(`email: (.+)$`) timestampRegex := regexp.MustCompile(`^(\d{4}/\d{2}/\d{2} \d{2}:\d{2}:\d{2})`) accessLogPath, _ := xray.GetAccessLogPath() file, _ := os.Open(accessLogPath) defer file.Close() // Track IPs with their last seen timestamp inboundClientIps := make(map[string]map[string]int64, 100) scanner := bufio.NewScanner(file) for scanner.Scan() { line := scanner.Text() ipMatches := ipRegex.FindStringSubmatch(line) if len(ipMatches) < 2 { continue } ip := ipMatches[1] if ip == "127.0.0.1" || ip == "::1" { continue } emailMatches := emailRegex.FindStringSubmatch(line) if len(emailMatches) < 2 { continue } email := emailMatches[1] // Extract timestamp from log line var timestamp int64 timestampMatches := timestampRegex.FindStringSubmatch(line) if len(timestampMatches) >= 2 { t, err := time.Parse("2006/01/02 15:04:05", timestampMatches[1]) if err == nil { timestamp = t.Unix() } else { timestamp = time.Now().Unix() } } else { timestamp = time.Now().Unix() } if _, exists := inboundClientIps[email]; !exists { inboundClientIps[email] = make(map[string]int64) } // Update timestamp - keep the latest if existingTime, ok := inboundClientIps[email][ip]; !ok || timestamp > existingTime { inboundClientIps[email][ip] = timestamp } } shouldCleanLog := false for email, ipTimestamps := range inboundClientIps { // Convert to IPWithTimestamp slice ipsWithTime := make([]IPWithTimestamp, 0, len(ipTimestamps)) for ip, timestamp := range ipTimestamps { ipsWithTime = append(ipsWithTime, IPWithTimestamp{IP: ip, Timestamp: timestamp}) } clientIpsRecord, err := j.getInboundClientIps(email) if err != nil { j.addInboundClientIps(email, ipsWithTime) continue } shouldCleanLog = j.updateInboundClientIps(clientIpsRecord, email, ipsWithTime) || shouldCleanLog } return shouldCleanLog } func (j *CheckClientIpJob) checkFail2BanInstalled() bool { cmd := "fail2ban-client" args := []string{"-h"} err := exec.Command(cmd, args...).Run() return err == nil } func (j *CheckClientIpJob) checkAccessLogAvailable(iplimitActive bool) bool { accessLogPath, err := xray.GetAccessLogPath() if err != nil { return false } if accessLogPath == "none" || accessLogPath == "" { if iplimitActive { logger.Warning("[LimitIP] Access log path is not set, Please configure the access log path in Xray configs.") } return false } return true } func (j *CheckClientIpJob) checkError(e error) { if e != nil { logger.Warning("client ip job err:", e) } } func (j *CheckClientIpJob) getInboundClientIps(clientEmail string) (*model.InboundClientIps, error) { db := database.GetDB() InboundClientIps := &model.InboundClientIps{} err := db.Model(model.InboundClientIps{}).Where("client_email = ?", clientEmail).First(InboundClientIps).Error if err != nil { return nil, err } return InboundClientIps, nil } func (j *CheckClientIpJob) addInboundClientIps(clientEmail string, ipsWithTime []IPWithTimestamp) error { inboundClientIps := &model.InboundClientIps{} jsonIps, err := json.Marshal(ipsWithTime) j.checkError(err) inboundClientIps.ClientEmail = clientEmail inboundClientIps.Ips = string(jsonIps) db := database.GetDB() tx := db.Begin() defer func() { if err == nil { tx.Commit() } else { tx.Rollback() } }() err = tx.Save(inboundClientIps).Error if err != nil { return err } return nil } func (j *CheckClientIpJob) updateInboundClientIps(inboundClientIps *model.InboundClientIps, clientEmail string, newIpsWithTime []IPWithTimestamp) bool { // Get the inbound configuration inbound, err := j.getInboundByEmail(clientEmail) if err != nil { logger.Errorf("failed to fetch inbound settings for email %s: %s", clientEmail, err) return false } if inbound.Settings == "" { logger.Debug("wrong data:", inbound) return false } settings := map[string][]model.Client{} json.Unmarshal([]byte(inbound.Settings), &settings) clients := settings["clients"] // Find the client's IP limit var limitIp int var clientFound bool for _, client := range clients { if client.Email == clientEmail { limitIp = client.LimitIP clientFound = true break } } if !clientFound || limitIp <= 0 || !inbound.Enable { // No limit or inbound disabled, just update and return jsonIps, _ := json.Marshal(newIpsWithTime) inboundClientIps.Ips = string(jsonIps) db := database.GetDB() db.Save(inboundClientIps) return false } // Parse old IPs from database var oldIpsWithTime []IPWithTimestamp if inboundClientIps.Ips != "" { json.Unmarshal([]byte(inboundClientIps.Ips), &oldIpsWithTime) } // Merge old and new IPs, keeping the latest timestamp for each IP ipMap := make(map[string]int64) for _, ipTime := range oldIpsWithTime { ipMap[ipTime.IP] = ipTime.Timestamp } for _, ipTime := range newIpsWithTime { if existingTime, ok := ipMap[ipTime.IP]; !ok || ipTime.Timestamp > existingTime { ipMap[ipTime.IP] = ipTime.Timestamp } } // Convert back to slice and sort by timestamp (oldest first) // This ensures we always protect the original/current connections and ban new excess ones. allIps := make([]IPWithTimestamp, 0, len(ipMap)) for ip, timestamp := range ipMap { allIps = append(allIps, IPWithTimestamp{IP: ip, Timestamp: timestamp}) } sort.Slice(allIps, func(i, j int) bool { return allIps[i].Timestamp < allIps[j].Timestamp // Ascending order (oldest first) }) shouldCleanLog := false j.disAllowedIps = []string{} // Open log file logIpFile, err := os.OpenFile(xray.GetIPLimitLogPath(), os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644) if err != nil { logger.Errorf("failed to open IP limit log file: %s", err) return false } defer logIpFile.Close() log.SetOutput(logIpFile) log.SetFlags(log.LstdFlags) // Check if we exceed the limit if len(allIps) > limitIp { shouldCleanLog = true // Keep the oldest IPs (currently active connections) and ban the new excess ones. keptIps := allIps[:limitIp] bannedIps := allIps[limitIp:] // Log banned IPs in the format fail2ban filters expect: [LIMIT_IP] Email = X || Disconnecting OLD IP = Y || Timestamp = Z for _, ipTime := range bannedIps { j.disAllowedIps = append(j.disAllowedIps, ipTime.IP) log.Printf("[LIMIT_IP] Email = %s || Disconnecting OLD IP = %s || Timestamp = %d", clientEmail, ipTime.IP, ipTime.Timestamp) } // Update database with only the currently active (kept) IPs jsonIps, _ := json.Marshal(keptIps) inboundClientIps.Ips = string(jsonIps) } else { // Under limit, save all IPs jsonIps, _ := json.Marshal(allIps) inboundClientIps.Ips = string(jsonIps) } db := database.GetDB() err = db.Save(inboundClientIps).Error if err != nil { logger.Error("failed to save inboundClientIps:", err) return false } if len(j.disAllowedIps) > 0 { logger.Infof("[LIMIT_IP] Client %s: Kept %d current IPs, queued %d new IPs for fail2ban", clientEmail, limitIp, len(j.disAllowedIps)) } return shouldCleanLog } func (j *CheckClientIpJob) getInboundByEmail(clientEmail string) (*model.Inbound, error) { db := database.GetDB() inbound := &model.Inbound{} err := db.Model(&model.Inbound{}).Where("settings LIKE ?", "%"+clientEmail+"%").First(inbound).Error if err != nil { return nil, err } return inbound, nil } ================================================ FILE: web/job/check_cpu_usage.go ================================================ package job import ( "strconv" "time" "github.com/mhsanaei/3x-ui/v2/web/service" "github.com/shirou/gopsutil/v4/cpu" ) // CheckCpuJob monitors CPU usage and sends Telegram notifications when usage exceeds the configured threshold. type CheckCpuJob struct { tgbotService service.Tgbot settingService service.SettingService } // NewCheckCpuJob creates a new CPU monitoring job instance. func NewCheckCpuJob() *CheckCpuJob { return new(CheckCpuJob) } // Run checks CPU usage over the last minute and sends a Telegram alert if it exceeds the threshold. func (j *CheckCpuJob) Run() { threshold, err := j.settingService.GetTgCpu() if err != nil || threshold <= 0 { // If threshold cannot be retrieved or is not set, skip sending notifications return } // get latest status of server percent, err := cpu.Percent(1*time.Minute, false) if err == nil && percent[0] > float64(threshold) { msg := j.tgbotService.I18nBot("tgbot.messages.cpuThreshold", "Percent=="+strconv.FormatFloat(percent[0], 'f', 2, 64), "Threshold=="+strconv.Itoa(threshold)) j.tgbotService.SendMsgToTgbotAdmins(msg) } } ================================================ FILE: web/job/check_hash_storage.go ================================================ package job import ( "github.com/mhsanaei/3x-ui/v2/web/service" ) // CheckHashStorageJob periodically cleans up expired hash entries from the Telegram bot's hash storage. type CheckHashStorageJob struct { tgbotService service.Tgbot } // NewCheckHashStorageJob creates a new hash storage cleanup job instance. func NewCheckHashStorageJob() *CheckHashStorageJob { return new(CheckHashStorageJob) } // Run removes expired hash entries from the Telegram bot's hash storage. func (j *CheckHashStorageJob) Run() { // Remove expired hashes from storage j.tgbotService.GetHashStorage().RemoveExpiredHashes() } ================================================ FILE: web/job/check_xray_running_job.go ================================================ // Package job provides background job implementations for the 3x-ui web panel, // including traffic monitoring, system checks, and periodic maintenance tasks. package job import ( "github.com/mhsanaei/3x-ui/v2/logger" "github.com/mhsanaei/3x-ui/v2/web/service" ) // CheckXrayRunningJob monitors Xray process health and restarts it if it crashes. type CheckXrayRunningJob struct { xrayService service.XrayService checkTime int } // NewCheckXrayRunningJob creates a new Xray health check job instance. func NewCheckXrayRunningJob() *CheckXrayRunningJob { return new(CheckXrayRunningJob) } // Run checks if Xray has crashed and restarts it after confirming it's down for 2 consecutive checks. func (j *CheckXrayRunningJob) Run() { if !j.xrayService.DidXrayCrash() { j.checkTime = 0 } else { j.checkTime++ // only restart if it's down 2 times in a row if j.checkTime > 1 { err := j.xrayService.RestartXray(false) j.checkTime = 0 if err != nil { logger.Error("Restart xray failed:", err) } } } } ================================================ FILE: web/job/clear_logs_job.go ================================================ package job import ( "io" "os" "path/filepath" "github.com/mhsanaei/3x-ui/v2/logger" "github.com/mhsanaei/3x-ui/v2/xray" ) // ClearLogsJob clears old log files to prevent disk space issues. type ClearLogsJob struct{} // NewClearLogsJob creates a new log cleanup job instance. func NewClearLogsJob() *ClearLogsJob { return new(ClearLogsJob) } // ensureFileExists creates the necessary directories and file if they don't exist func ensureFileExists(path string) error { dir := filepath.Dir(path) if err := os.MkdirAll(dir, 0755); err != nil { return err } file, err := os.OpenFile(path, os.O_CREATE|os.O_RDWR, 0644) if err != nil { return err } file.Close() return nil } // Here Run is an interface method of the Job interface func (j *ClearLogsJob) Run() { logFiles := []string{xray.GetIPLimitLogPath(), xray.GetIPLimitBannedLogPath(), xray.GetAccessPersistentLogPath()} logFilesPrev := []string{xray.GetIPLimitBannedPrevLogPath(), xray.GetAccessPersistentPrevLogPath()} // Ensure all log files and their paths exist for _, path := range append(logFiles, logFilesPrev...) { if err := ensureFileExists(path); err != nil { logger.Warning("Failed to ensure log file exists:", path, "-", err) } } // Clear log files and copy to previous logs for i := range len(logFiles) { if i > 0 { // Copy to previous logs logFilePrev, err := os.OpenFile(logFilesPrev[i-1], os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0644) if err != nil { logger.Warning("Failed to open previous log file for writing:", logFilesPrev[i-1], "-", err) continue } logFile, err := os.OpenFile(logFiles[i], os.O_RDONLY, 0644) if err != nil { logger.Warning("Failed to open current log file for reading:", logFiles[i], "-", err) logFilePrev.Close() continue } _, err = io.Copy(logFilePrev, logFile) if err != nil { logger.Warning("Failed to copy log file:", logFiles[i], "to", logFilesPrev[i-1], "-", err) } logFile.Close() logFilePrev.Close() } err := os.Truncate(logFiles[i], 0) if err != nil { logger.Warning("Failed to truncate log file:", logFiles[i], "-", err) } } } ================================================ FILE: web/job/ldap_sync_job.go ================================================ package job import ( "time" "strings" "github.com/mhsanaei/3x-ui/v2/database/model" "github.com/mhsanaei/3x-ui/v2/logger" ldaputil "github.com/mhsanaei/3x-ui/v2/util/ldap" "github.com/mhsanaei/3x-ui/v2/web/service" "strconv" "github.com/google/uuid" ) var DefaultTruthyValues = []string{"true", "1", "yes", "on"} type LdapSyncJob struct { settingService service.SettingService inboundService service.InboundService xrayService service.XrayService } // --- Helper functions for mustGet --- func mustGetString(fn func() (string, error)) string { v, err := fn() if err != nil { panic(err) } return v } func mustGetInt(fn func() (int, error)) int { v, err := fn() if err != nil { panic(err) } return v } func mustGetBool(fn func() (bool, error)) bool { v, err := fn() if err != nil { panic(err) } return v } func mustGetStringOr(fn func() (string, error), fallback string) string { v, err := fn() if err != nil || v == "" { return fallback } return v } func NewLdapSyncJob() *LdapSyncJob { return new(LdapSyncJob) } func (j *LdapSyncJob) Run() { logger.Info("LDAP sync job started") enabled, err := j.settingService.GetLdapEnable() if err != nil || !enabled { logger.Warning("LDAP disabled or failed to fetch flag") return } // --- LDAP fetch --- cfg := ldaputil.Config{ Host: mustGetString(j.settingService.GetLdapHost), Port: mustGetInt(j.settingService.GetLdapPort), UseTLS: mustGetBool(j.settingService.GetLdapUseTLS), BindDN: mustGetString(j.settingService.GetLdapBindDN), Password: mustGetString(j.settingService.GetLdapPassword), BaseDN: mustGetString(j.settingService.GetLdapBaseDN), UserFilter: mustGetString(j.settingService.GetLdapUserFilter), UserAttr: mustGetString(j.settingService.GetLdapUserAttr), FlagField: mustGetStringOr(j.settingService.GetLdapFlagField, mustGetString(j.settingService.GetLdapVlessField)), TruthyVals: splitCsv(mustGetString(j.settingService.GetLdapTruthyValues)), Invert: mustGetBool(j.settingService.GetLdapInvertFlag), } flags, err := ldaputil.FetchVlessFlags(cfg) if err != nil { logger.Warning("LDAP fetch failed:", err) return } logger.Infof("Fetched %d LDAP flags", len(flags)) // --- Load all inbounds and all clients once --- inboundTags := splitCsv(mustGetString(j.settingService.GetLdapInboundTags)) inbounds, err := j.inboundService.GetAllInbounds() if err != nil { logger.Warning("Failed to get inbounds:", err) return } allClients := map[string]*model.Client{} // email -> client inboundMap := map[string]*model.Inbound{} // tag -> inbound for _, ib := range inbounds { inboundMap[ib.Tag] = ib clients, _ := j.inboundService.GetClients(ib) for i := range clients { allClients[clients[i].Email] = &clients[i] } } // --- Prepare batch operations --- autoCreate := mustGetBool(j.settingService.GetLdapAutoCreate) defGB := mustGetInt(j.settingService.GetLdapDefaultTotalGB) defExpiryDays := mustGetInt(j.settingService.GetLdapDefaultExpiryDays) defLimitIP := mustGetInt(j.settingService.GetLdapDefaultLimitIP) clientsToCreate := map[string][]model.Client{} // tag -> []new clients clientsToEnable := map[string][]string{} // tag -> []email clientsToDisable := map[string][]string{} // tag -> []email for email, allowed := range flags { exists := allClients[email] != nil for _, tag := range inboundTags { if !exists && allowed && autoCreate { newClient := j.buildClient(inboundMap[tag], email, defGB, defExpiryDays, defLimitIP) clientsToCreate[tag] = append(clientsToCreate[tag], newClient) } else if exists { if allowed && !allClients[email].Enable { clientsToEnable[tag] = append(clientsToEnable[tag], email) } else if !allowed && allClients[email].Enable { clientsToDisable[tag] = append(clientsToDisable[tag], email) } } } } // --- Execute batch create --- for tag, newClients := range clientsToCreate { if len(newClients) == 0 { continue } payload := &model.Inbound{Id: inboundMap[tag].Id} payload.Settings = j.clientsToJSON(newClients) if _, err := j.inboundService.AddInboundClient(payload); err != nil { logger.Warningf("Failed to add clients for tag %s: %v", tag, err) } else { logger.Infof("LDAP auto-create: %d clients for %s", len(newClients), tag) j.xrayService.SetToNeedRestart() } } // --- Execute enable/disable batch --- for tag, emails := range clientsToEnable { j.batchSetEnable(inboundMap[tag], emails, true) } for tag, emails := range clientsToDisable { j.batchSetEnable(inboundMap[tag], emails, false) } // --- Auto delete clients not in LDAP --- autoDelete := mustGetBool(j.settingService.GetLdapAutoDelete) if autoDelete { ldapEmailSet := map[string]struct{}{} for e := range flags { ldapEmailSet[e] = struct{}{} } for _, tag := range inboundTags { j.deleteClientsNotInLDAP(tag, ldapEmailSet) } } } func splitCsv(s string) []string { if s == "" { return DefaultTruthyValues } parts := strings.Split(s, ",") out := make([]string, 0, len(parts)) for _, p := range parts { v := strings.TrimSpace(p) if v != "" { out = append(out, v) } } return out } // buildClient creates a new client for auto-create func (j *LdapSyncJob) buildClient(ib *model.Inbound, email string, defGB, defExpiryDays, defLimitIP int) model.Client { c := model.Client{ Email: email, Enable: true, LimitIP: defLimitIP, TotalGB: int64(defGB), } if defExpiryDays > 0 { c.ExpiryTime = time.Now().Add(time.Duration(defExpiryDays) * 24 * time.Hour).UnixMilli() } switch ib.Protocol { case model.Trojan, model.Shadowsocks: c.Password = uuid.NewString() default: c.ID = uuid.NewString() } return c } // batchSetEnable enables/disables clients in batch through a single call func (j *LdapSyncJob) batchSetEnable(ib *model.Inbound, emails []string, enable bool) { if len(emails) == 0 { return } // Prepare JSON for mass update clients := make([]model.Client, 0, len(emails)) for _, email := range emails { clients = append(clients, model.Client{ Email: email, Enable: enable, }) } payload := &model.Inbound{ Id: ib.Id, Settings: j.clientsToJSON(clients), } // Use a single AddInboundClient call to update enable if _, err := j.inboundService.AddInboundClient(payload); err != nil { logger.Warningf("Batch set enable failed for inbound %s: %v", ib.Tag, err) return } logger.Infof("Batch set enable=%v for %d clients in inbound %s", enable, len(emails), ib.Tag) j.xrayService.SetToNeedRestart() } // deleteClientsNotInLDAP deletes clients not in LDAP using batches and a single restart func (j *LdapSyncJob) deleteClientsNotInLDAP(inboundTag string, ldapEmails map[string]struct{}) { inbounds, err := j.inboundService.GetAllInbounds() if err != nil { logger.Warning("Failed to get inbounds for deletion:", err) return } batchSize := 50 // clients in 1 batch restartNeeded := false for _, ib := range inbounds { if ib.Tag != inboundTag { continue } clients, err := j.inboundService.GetClients(ib) if err != nil { logger.Warningf("Failed to get clients for inbound %s: %v", ib.Tag, err) continue } // Collect clients for deletion toDelete := []model.Client{} for _, c := range clients { if _, ok := ldapEmails[c.Email]; !ok { toDelete = append(toDelete, c) } } if len(toDelete) == 0 { continue } // Delete in batches for i := 0; i < len(toDelete); i += batchSize { end := min(i+batchSize, len(toDelete)) batch := toDelete[i:end] for _, c := range batch { var clientKey string switch ib.Protocol { case model.Trojan: clientKey = c.Password case model.Shadowsocks: clientKey = c.Email default: // vless/vmess clientKey = c.ID } if _, err := j.inboundService.DelInboundClient(ib.Id, clientKey); err != nil { logger.Warningf("Failed to delete client %s from inbound id=%d(tag=%s): %v", c.Email, ib.Id, ib.Tag, err) } else { logger.Infof("Deleted client %s from inbound id=%d(tag=%s)", c.Email, ib.Id, ib.Tag) // do not restart here restartNeeded = true } } } } // One time after all batches if restartNeeded { j.xrayService.SetToNeedRestart() logger.Info("Xray restart scheduled after batch deletion") } } // clientsToJSON serializes an array of clients to JSON func (j *LdapSyncJob) clientsToJSON(clients []model.Client) string { b := strings.Builder{} b.WriteString("{\"clients\":[") for i, c := range clients { if i > 0 { b.WriteString(",") } b.WriteString(j.clientToJSON(c)) } b.WriteString("]}") return b.String() } // clientToJSON serializes minimal client fields to JSON object string without extra deps func (j *LdapSyncJob) clientToJSON(c model.Client) string { // construct minimal JSON manually to avoid importing json for simple case b := strings.Builder{} b.WriteString("{") if c.ID != "" { b.WriteString("\"id\":\"") b.WriteString(c.ID) b.WriteString("\",") } if c.Password != "" { b.WriteString("\"password\":\"") b.WriteString(c.Password) b.WriteString("\",") } b.WriteString("\"email\":\"") b.WriteString(c.Email) b.WriteString("\",") b.WriteString("\"enable\":") if c.Enable { b.WriteString("true") } else { b.WriteString("false") } b.WriteString(",") b.WriteString("\"limitIp\":") b.WriteString(strconv.Itoa(c.LimitIP)) b.WriteString(",") b.WriteString("\"totalGB\":") b.WriteString(strconv.FormatInt(c.TotalGB, 10)) if c.ExpiryTime > 0 { b.WriteString(",\"expiryTime\":") b.WriteString(strconv.FormatInt(c.ExpiryTime, 10)) } b.WriteString("}") return b.String() } ================================================ FILE: web/job/periodic_traffic_reset_job.go ================================================ package job import ( "github.com/mhsanaei/3x-ui/v2/logger" "github.com/mhsanaei/3x-ui/v2/web/service" ) // Period represents the time period for traffic resets. type Period string // PeriodicTrafficResetJob resets traffic statistics for inbounds based on their configured reset period. type PeriodicTrafficResetJob struct { inboundService service.InboundService period Period } // NewPeriodicTrafficResetJob creates a new periodic traffic reset job for the specified period. func NewPeriodicTrafficResetJob(period Period) *PeriodicTrafficResetJob { return &PeriodicTrafficResetJob{ period: period, } } // Run resets traffic statistics for all inbounds that match the configured reset period. func (j *PeriodicTrafficResetJob) Run() { inbounds, err := j.inboundService.GetInboundsByTrafficReset(string(j.period)) if err != nil { logger.Warning("Failed to get inbounds for traffic reset:", err) return } if len(inbounds) == 0 { return } logger.Infof("Running periodic traffic reset job for period: %s (%d matching inbounds)", j.period, len(inbounds)) resetCount := 0 for _, inbound := range inbounds { resetInboundErr := j.inboundService.ResetAllTraffics() if resetInboundErr != nil { logger.Warning("Failed to reset traffic for inbound", inbound.Id, ":", resetInboundErr) } resetClientErr := j.inboundService.ResetAllClientTraffics(inbound.Id) if resetClientErr != nil { logger.Warning("Failed to reset traffic for all users of inbound", inbound.Id, ":", resetClientErr) } if resetInboundErr == nil && resetClientErr == nil { resetCount++ } } if resetCount > 0 { logger.Infof("Periodic traffic reset completed: %d inbounds reset", resetCount) } } ================================================ FILE: web/job/stats_notify_job.go ================================================ package job import ( "github.com/mhsanaei/3x-ui/v2/web/service" ) // LoginStatus represents the status of a login attempt. type LoginStatus byte const ( LoginSuccess LoginStatus = 1 // Successful login LoginFail LoginStatus = 0 // Failed login attempt ) // StatsNotifyJob sends periodic statistics reports via Telegram bot. type StatsNotifyJob struct { xrayService service.XrayService tgbotService service.Tgbot } // NewStatsNotifyJob creates a new statistics notification job instance. func NewStatsNotifyJob() *StatsNotifyJob { return new(StatsNotifyJob) } // Run sends a statistics report via Telegram bot if Xray is running. func (j *StatsNotifyJob) Run() { if !j.xrayService.IsXrayRunning() { return } j.tgbotService.SendReport() } ================================================ FILE: web/job/xray_traffic_job.go ================================================ package job import ( "encoding/json" "github.com/mhsanaei/3x-ui/v2/logger" "github.com/mhsanaei/3x-ui/v2/web/service" "github.com/mhsanaei/3x-ui/v2/web/websocket" "github.com/mhsanaei/3x-ui/v2/xray" "github.com/valyala/fasthttp" ) // XrayTrafficJob collects and processes traffic statistics from Xray, updating the database and optionally informing external APIs. type XrayTrafficJob struct { settingService service.SettingService xrayService service.XrayService inboundService service.InboundService outboundService service.OutboundService } // NewXrayTrafficJob creates a new traffic collection job instance. func NewXrayTrafficJob() *XrayTrafficJob { return new(XrayTrafficJob) } // Run collects traffic statistics from Xray and updates the database, triggering restart if needed. func (j *XrayTrafficJob) Run() { if !j.xrayService.IsXrayRunning() { return } traffics, clientTraffics, err := j.xrayService.GetXrayTraffic() if err != nil { return } err, needRestart0 := j.inboundService.AddTraffic(traffics, clientTraffics) if err != nil { logger.Warning("add inbound traffic failed:", err) } err, needRestart1 := j.outboundService.AddTraffic(traffics, clientTraffics) if err != nil { logger.Warning("add outbound traffic failed:", err) } if ExternalTrafficInformEnable, err := j.settingService.GetExternalTrafficInformEnable(); ExternalTrafficInformEnable { j.informTrafficToExternalAPI(traffics, clientTraffics) } else if err != nil { logger.Warning("get ExternalTrafficInformEnable failed:", err) } if needRestart0 || needRestart1 { j.xrayService.SetToNeedRestart() } // Get online clients and last online map for real-time status updates onlineClients := j.inboundService.GetOnlineClients() lastOnlineMap, err := j.inboundService.GetClientsLastOnline() if err != nil { logger.Warning("get clients last online failed:", err) lastOnlineMap = make(map[string]int64) } // Fetch updated inbounds from database with accumulated traffic values // This ensures frontend receives the actual total traffic, not just delta values updatedInbounds, err := j.inboundService.GetAllInbounds() if err != nil { logger.Warning("get all inbounds for websocket failed:", err) } updatedOutbounds, err := j.outboundService.GetOutboundsTraffic() if err != nil { logger.Warning("get all outbounds for websocket failed:", err) } // Broadcast traffic update via WebSocket with accumulated values from database trafficUpdate := map[string]any{ "traffics": traffics, "clientTraffics": clientTraffics, "onlineClients": onlineClients, "lastOnlineMap": lastOnlineMap, } websocket.BroadcastTraffic(trafficUpdate) // Broadcast full inbounds update for real-time UI refresh if updatedInbounds != nil { websocket.BroadcastInbounds(updatedInbounds) } if updatedOutbounds != nil { websocket.BroadcastOutbounds(updatedOutbounds) } } func (j *XrayTrafficJob) informTrafficToExternalAPI(inboundTraffics []*xray.Traffic, clientTraffics []*xray.ClientTraffic) { informURL, err := j.settingService.GetExternalTrafficInformURI() if err != nil { logger.Warning("get ExternalTrafficInformURI failed:", err) return } requestBody, err := json.Marshal(map[string]any{"clientTraffics": clientTraffics, "inboundTraffics": inboundTraffics}) if err != nil { logger.Warning("parse client/inbound traffic failed:", err) return } request := fasthttp.AcquireRequest() defer fasthttp.ReleaseRequest(request) request.Header.SetMethod("POST") request.Header.SetContentType("application/json; charset=UTF-8") request.SetBody([]byte(requestBody)) request.SetRequestURI(informURL) response := fasthttp.AcquireResponse() defer fasthttp.ReleaseResponse(response) if err := fasthttp.Do(request, response); err != nil { logger.Warning("POST ExternalTrafficInformURI failed:", err) } } ================================================ FILE: web/locale/locale.go ================================================ // Package locale provides internationalization (i18n) support for the 3x-ui web panel, // including translation loading, localization, and middleware for web and bot interfaces. package locale import ( "embed" "io/fs" "os" "strings" "github.com/mhsanaei/3x-ui/v2/logger" "github.com/gin-gonic/gin" "github.com/nicksnyder/go-i18n/v2/i18n" "github.com/pelletier/go-toml/v2" "golang.org/x/text/language" ) var ( i18nBundle *i18n.Bundle LocalizerWeb *i18n.Localizer LocalizerBot *i18n.Localizer ) // I18nType represents the type of interface for internationalization. type I18nType string const ( Bot I18nType = "bot" // Bot interface type Web I18nType = "web" // Web interface type ) // SettingService interface defines methods for accessing locale settings. type SettingService interface { GetTgLang() (string, error) } // InitLocalizer initializes the internationalization system with embedded translation files. func InitLocalizer(i18nFS embed.FS, settingService SettingService) error { // set default bundle to English i18nBundle = i18n.NewBundle(language.MustParse("en-US")) i18nBundle.RegisterUnmarshalFunc("toml", toml.Unmarshal) // parse files if err := parseTranslationFiles(i18nFS, i18nBundle); err != nil { return err } // setup bot locale if err := initTGBotLocalizer(settingService); err != nil { return err } return nil } // createTemplateData creates a template data map from parameters with optional separator. func createTemplateData(params []string, separator ...string) map[string]any { var sep string = "==" if len(separator) > 0 { sep = separator[0] } templateData := make(map[string]any) for _, param := range params { parts := strings.SplitN(param, sep, 2) templateData[parts[0]] = parts[1] } return templateData } // I18n retrieves a localized message for the given key and type. // It supports both bot and web contexts, with optional template parameters. // Returns the localized message or an empty string if localization fails. func I18n(i18nType I18nType, key string, params ...string) string { var localizer *i18n.Localizer switch i18nType { case "bot": localizer = LocalizerBot case "web": localizer = LocalizerWeb default: logger.Errorf("Invalid type for I18n: %s", i18nType) return "" } templateData := createTemplateData(params) if localizer == nil { // Fallback to key if localizer not ready; prevents nil panic on pages like sub return key } msg, err := localizer.Localize(&i18n.LocalizeConfig{ MessageID: key, TemplateData: templateData, }) if err != nil { logger.Errorf("Failed to localize message: %v", err) return "" } return msg } // initTGBotLocalizer initializes the bot localizer with the configured language. func initTGBotLocalizer(settingService SettingService) error { botLang, err := settingService.GetTgLang() if err != nil { return err } LocalizerBot = i18n.NewLocalizer(i18nBundle, botLang) return nil } // LocalizerMiddleware returns a Gin middleware that sets up localization for web requests. // It determines the user's language from cookies or Accept-Language header, // creates a localizer instance, and stores it in the Gin context for use in handlers. // Also provides the I18n function in the context for template rendering. func LocalizerMiddleware() gin.HandlerFunc { return func(c *gin.Context) { // Ensure bundle is initialized so creating a Localizer won't panic if i18nBundle == nil { i18nBundle = i18n.NewBundle(language.MustParse("en-US")) i18nBundle.RegisterUnmarshalFunc("toml", toml.Unmarshal) // Try lazy-load from disk when running sub server without InitLocalizer if err := loadTranslationsFromDisk(i18nBundle); err != nil { logger.Warning("i18n lazy load failed:", err) } } var lang string if cookie, err := c.Request.Cookie("lang"); err == nil { lang = cookie.Value } else { lang = c.GetHeader("Accept-Language") } LocalizerWeb = i18n.NewLocalizer(i18nBundle, lang) c.Set("localizer", LocalizerWeb) c.Set("I18n", I18n) c.Next() } } // loadTranslationsFromDisk attempts to load translation files from "web/translation" using the local filesystem. func loadTranslationsFromDisk(bundle *i18n.Bundle) error { root := os.DirFS("web") return fs.WalkDir(root, "translation", func(path string, d fs.DirEntry, err error) error { if err != nil { return err } if d.IsDir() { return nil } data, err := fs.ReadFile(root, path) if err != nil { return err } _, err = bundle.ParseMessageFileBytes(data, path) return err }) } // parseTranslationFiles parses embedded translation files and adds them to the i18n bundle. func parseTranslationFiles(i18nFS embed.FS, i18nBundle *i18n.Bundle) error { err := fs.WalkDir(i18nFS, "translation", func(path string, d fs.DirEntry, err error) error { if err != nil { return err } if d.IsDir() { return nil } data, err := i18nFS.ReadFile(path) if err != nil { return err } _, err = i18nBundle.ParseMessageFileBytes(data, path) return err }) if err != nil { return err } return nil } ================================================ FILE: web/middleware/domainValidator.go ================================================ // Package middleware provides HTTP middleware functions for the 3x-ui web panel, // including domain validation and URL redirection utilities. package middleware import ( "net" "net/http" "strings" "github.com/gin-gonic/gin" ) // DomainValidatorMiddleware returns a Gin middleware that validates the request domain. // It extracts the host from the request, strips any port number, and compares it // against the configured domain. Requests from unauthorized domains are rejected // with HTTP 403 Forbidden status. func DomainValidatorMiddleware(domain string) gin.HandlerFunc { return func(c *gin.Context) { host := c.Request.Host if colonIndex := strings.LastIndex(host, ":"); colonIndex != -1 { host, _, _ = net.SplitHostPort(c.Request.Host) } if host != domain { c.AbortWithStatus(http.StatusForbidden) return } c.Next() } } ================================================ FILE: web/middleware/redirect.go ================================================ package middleware import ( "net/http" "strings" "github.com/gin-gonic/gin" ) // RedirectMiddleware returns a Gin middleware that handles URL redirections. // It provides backward compatibility by redirecting old '/xui' paths to new '/panel' paths, // including API endpoints. The middleware performs permanent redirects (301) for SEO purposes. func RedirectMiddleware(basePath string) gin.HandlerFunc { return func(c *gin.Context) { // Redirect from old '/xui' path to '/panel' redirects := map[string]string{ "panel/API": "panel/api", "xui/API": "panel/api", "xui": "panel", } path := c.Request.URL.Path for from, to := range redirects { from, to = basePath+from, basePath+to if strings.HasPrefix(path, from) { newPath := to + path[len(from):] c.Redirect(http.StatusMovedPermanently, newPath) c.Abort() return } } c.Next() } } ================================================ FILE: web/network/auto_https_conn.go ================================================ // Package network provides network utilities for the 3x-ui web panel, // including automatic HTTP to HTTPS redirection functionality. package network import ( "bufio" "bytes" "fmt" "net" "net/http" "sync" ) // AutoHttpsConn wraps a net.Conn to provide automatic HTTP to HTTPS redirection. // It intercepts the first read to detect HTTP requests and responds with a 307 redirect // to the HTTPS equivalent URL. Subsequent reads work normally for HTTPS connections. type AutoHttpsConn struct { net.Conn firstBuf []byte bufStart int readRequestOnce sync.Once } // NewAutoHttpsConn creates a new AutoHttpsConn that wraps the given connection. // It enables automatic redirection of HTTP requests to HTTPS. func NewAutoHttpsConn(conn net.Conn) net.Conn { return &AutoHttpsConn{ Conn: conn, } } func (c *AutoHttpsConn) readRequest() bool { c.firstBuf = make([]byte, 2048) n, err := c.Conn.Read(c.firstBuf) c.firstBuf = c.firstBuf[:n] if err != nil { return false } reader := bytes.NewReader(c.firstBuf) bufReader := bufio.NewReader(reader) request, err := http.ReadRequest(bufReader) if err != nil { return false } resp := http.Response{ Header: http.Header{}, } resp.StatusCode = http.StatusTemporaryRedirect location := fmt.Sprintf("https://%v%v", request.Host, request.RequestURI) resp.Header.Set("Location", location) resp.Write(c.Conn) c.Close() c.firstBuf = nil return true } // Read implements the net.Conn Read method with automatic HTTPS redirection. // On the first read, it checks if the request is HTTP and redirects to HTTPS if so. // Subsequent reads work normally. func (c *AutoHttpsConn) Read(buf []byte) (int, error) { c.readRequestOnce.Do(func() { c.readRequest() }) if c.firstBuf != nil { n := copy(buf, c.firstBuf[c.bufStart:]) c.bufStart += n if c.bufStart >= len(c.firstBuf) { c.firstBuf = nil } return n, nil } return c.Conn.Read(buf) } ================================================ FILE: web/network/auto_https_listener.go ================================================ package network import "net" // AutoHttpsListener wraps a net.Listener to provide automatic HTTPS redirection. // It returns AutoHttpsConn connections that handle HTTP to HTTPS redirection. type AutoHttpsListener struct { net.Listener } // NewAutoHttpsListener creates a new AutoHttpsListener that wraps the given listener. // It enables automatic redirection of HTTP requests to HTTPS for all accepted connections. func NewAutoHttpsListener(listener net.Listener) net.Listener { return &AutoHttpsListener{ Listener: listener, } } // Accept implements the net.Listener Accept method. // It accepts connections and wraps them with AutoHttpsConn for HTTPS redirection. func (l *AutoHttpsListener) Accept() (net.Conn, error) { conn, err := l.Listener.Accept() if err != nil { return nil, err } return NewAutoHttpsConn(conn), nil } ================================================ FILE: web/service/config.json ================================================ { "log": { "access": "none", "dnsLog": false, "error": "", "loglevel": "warning", "maskAddress": "" }, "api": { "tag": "api", "services": [ "HandlerService", "LoggerService", "StatsService" ] }, "inbounds": [ { "tag": "api", "listen": "127.0.0.1", "port": 62789, "protocol": "tunnel", "settings": { "address": "127.0.0.1" } } ], "outbounds": [ { "tag": "direct", "protocol": "freedom", "settings": { "domainStrategy": "AsIs", "redirect": "", "noises": [] } }, { "tag": "blocked", "protocol": "blackhole", "settings": {} } ], "policy": { "levels": { "0": { "statsUserDownlink": true, "statsUserUplink": true } }, "system": { "statsInboundDownlink": true, "statsInboundUplink": true, "statsOutboundDownlink": false, "statsOutboundUplink": false } }, "routing": { "domainStrategy": "AsIs", "rules": [ { "type": "field", "inboundTag": [ "api" ], "outboundTag": "api" }, { "type": "field", "outboundTag": "blocked", "ip": [ "geoip:private" ] }, { "type": "field", "outboundTag": "blocked", "protocol": [ "bittorrent" ] } ] }, "stats": {}, "metrics": { "tag": "metrics_out", "listen": "127.0.0.1:11111" } } ================================================ FILE: web/service/inbound.go ================================================ // Package service provides business logic services for the 3x-ui web panel, // including inbound/outbound management, user administration, settings, and Xray integration. package service import ( "encoding/json" "fmt" "sort" "strconv" "strings" "time" "github.com/mhsanaei/3x-ui/v2/database" "github.com/mhsanaei/3x-ui/v2/database/model" "github.com/mhsanaei/3x-ui/v2/logger" "github.com/mhsanaei/3x-ui/v2/util/common" "github.com/mhsanaei/3x-ui/v2/xray" "gorm.io/gorm" ) // InboundService provides business logic for managing Xray inbound configurations. // It handles CRUD operations for inbounds, client management, traffic monitoring, // and integration with the Xray API for real-time updates. type InboundService struct { xrayApi xray.XrayAPI } // GetInbounds retrieves all inbounds for a specific user. // Returns a slice of inbound models with their associated client statistics. func (s *InboundService) GetInbounds(userId int) ([]*model.Inbound, error) { db := database.GetDB() var inbounds []*model.Inbound err := db.Model(model.Inbound{}).Preload("ClientStats").Where("user_id = ?", userId).Find(&inbounds).Error if err != nil && err != gorm.ErrRecordNotFound { return nil, err } // Enrich client stats with UUID/SubId from inbound settings for _, inbound := range inbounds { clients, _ := s.GetClients(inbound) if len(clients) == 0 || len(inbound.ClientStats) == 0 { continue } // Build a map email -> client cMap := make(map[string]model.Client, len(clients)) for _, c := range clients { cMap[strings.ToLower(c.Email)] = c } for i := range inbound.ClientStats { email := strings.ToLower(inbound.ClientStats[i].Email) if c, ok := cMap[email]; ok { inbound.ClientStats[i].UUID = c.ID inbound.ClientStats[i].SubId = c.SubID } } } return inbounds, nil } // GetAllInbounds retrieves all inbounds from the database. // Returns a slice of all inbound models with their associated client statistics. func (s *InboundService) GetAllInbounds() ([]*model.Inbound, error) { db := database.GetDB() var inbounds []*model.Inbound err := db.Model(model.Inbound{}).Preload("ClientStats").Find(&inbounds).Error if err != nil && err != gorm.ErrRecordNotFound { return nil, err } // Enrich client stats with UUID/SubId from inbound settings for _, inbound := range inbounds { clients, _ := s.GetClients(inbound) if len(clients) == 0 || len(inbound.ClientStats) == 0 { continue } cMap := make(map[string]model.Client, len(clients)) for _, c := range clients { cMap[strings.ToLower(c.Email)] = c } for i := range inbound.ClientStats { email := strings.ToLower(inbound.ClientStats[i].Email) if c, ok := cMap[email]; ok { inbound.ClientStats[i].UUID = c.ID inbound.ClientStats[i].SubId = c.SubID } } } return inbounds, nil } func (s *InboundService) GetInboundsByTrafficReset(period string) ([]*model.Inbound, error) { db := database.GetDB() var inbounds []*model.Inbound err := db.Model(model.Inbound{}).Where("traffic_reset = ?", period).Find(&inbounds).Error if err != nil && err != gorm.ErrRecordNotFound { return nil, err } return inbounds, nil } func (s *InboundService) checkPortExist(listen string, port int, ignoreId int) (bool, error) { db := database.GetDB() if listen == "" || listen == "0.0.0.0" || listen == "::" || listen == "::0" { db = db.Model(model.Inbound{}).Where("port = ?", port) } else { db = db.Model(model.Inbound{}). Where("port = ?", port). Where( db.Model(model.Inbound{}).Where( "listen = ?", listen, ).Or( "listen = \"\"", ).Or( "listen = \"0.0.0.0\"", ).Or( "listen = \"::\"", ).Or( "listen = \"::0\"")) } if ignoreId > 0 { db = db.Where("id != ?", ignoreId) } var count int64 err := db.Count(&count).Error if err != nil { return false, err } return count > 0, nil } func (s *InboundService) GetClients(inbound *model.Inbound) ([]model.Client, error) { settings := map[string][]model.Client{} json.Unmarshal([]byte(inbound.Settings), &settings) if settings == nil { return nil, fmt.Errorf("setting is null") } clients := settings["clients"] if clients == nil { return nil, nil } return clients, nil } func (s *InboundService) getAllEmails() ([]string, error) { db := database.GetDB() var emails []string err := db.Raw(` SELECT JSON_EXTRACT(client.value, '$.email') FROM inbounds, JSON_EACH(JSON_EXTRACT(inbounds.settings, '$.clients')) AS client `).Scan(&emails).Error if err != nil { return nil, err } return emails, nil } func (s *InboundService) contains(slice []string, str string) bool { lowerStr := strings.ToLower(str) for _, s := range slice { if strings.ToLower(s) == lowerStr { return true } } return false } func (s *InboundService) checkEmailsExistForClients(clients []model.Client) (string, error) { allEmails, err := s.getAllEmails() if err != nil { return "", err } var emails []string for _, client := range clients { if client.Email != "" { if s.contains(emails, client.Email) { return client.Email, nil } if s.contains(allEmails, client.Email) { return client.Email, nil } emails = append(emails, client.Email) } } return "", nil } func (s *InboundService) checkEmailExistForInbound(inbound *model.Inbound) (string, error) { clients, err := s.GetClients(inbound) if err != nil { return "", err } allEmails, err := s.getAllEmails() if err != nil { return "", err } var emails []string for _, client := range clients { if client.Email != "" { if s.contains(emails, client.Email) { return client.Email, nil } if s.contains(allEmails, client.Email) { return client.Email, nil } emails = append(emails, client.Email) } } return "", nil } // AddInbound creates a new inbound configuration. // It validates port uniqueness, client email uniqueness, and required fields, // then saves the inbound to the database and optionally adds it to the running Xray instance. // Returns the created inbound, whether Xray needs restart, and any error. func (s *InboundService) AddInbound(inbound *model.Inbound) (*model.Inbound, bool, error) { exist, err := s.checkPortExist(inbound.Listen, inbound.Port, 0) if err != nil { return inbound, false, err } if exist { return inbound, false, common.NewError("Port already exists:", inbound.Port) } existEmail, err := s.checkEmailExistForInbound(inbound) if err != nil { return inbound, false, err } if existEmail != "" { return inbound, false, common.NewError("Duplicate email:", existEmail) } clients, err := s.GetClients(inbound) if err != nil { return inbound, false, err } // Ensure created_at and updated_at on clients in settings if len(clients) > 0 { var settings map[string]any if err2 := json.Unmarshal([]byte(inbound.Settings), &settings); err2 == nil && settings != nil { now := time.Now().Unix() * 1000 updatedClients := make([]model.Client, 0, len(clients)) for _, c := range clients { if c.CreatedAt == 0 { c.CreatedAt = now } c.UpdatedAt = now updatedClients = append(updatedClients, c) } settings["clients"] = updatedClients if bs, err3 := json.MarshalIndent(settings, "", " "); err3 == nil { inbound.Settings = string(bs) } else { logger.Debug("Unable to marshal inbound settings with timestamps:", err3) } } else if err2 != nil { logger.Debug("Unable to parse inbound settings for timestamps:", err2) } } // Secure client ID for _, client := range clients { switch inbound.Protocol { case "trojan": if client.Password == "" { return inbound, false, common.NewError("empty client ID") } case "shadowsocks": if client.Email == "" { return inbound, false, common.NewError("empty client ID") } default: if client.ID == "" { return inbound, false, common.NewError("empty client ID") } } } db := database.GetDB() tx := db.Begin() defer func() { if err == nil { tx.Commit() } else { tx.Rollback() } }() err = tx.Save(inbound).Error if err == nil { if len(inbound.ClientStats) == 0 { for _, client := range clients { s.AddClientStat(tx, inbound.Id, &client) } } } else { return inbound, false, err } needRestart := false if inbound.Enable { s.xrayApi.Init(p.GetAPIPort()) inboundJson, err1 := json.MarshalIndent(inbound.GenXrayInboundConfig(), "", " ") if err1 != nil { logger.Debug("Unable to marshal inbound config:", err1) } err1 = s.xrayApi.AddInbound(inboundJson) if err1 == nil { logger.Debug("New inbound added by api:", inbound.Tag) } else { logger.Debug("Unable to add inbound by api:", err1) needRestart = true } s.xrayApi.Close() } return inbound, needRestart, err } // DelInbound deletes an inbound configuration by ID. // It removes the inbound from the database and the running Xray instance if active. // Returns whether Xray needs restart and any error. func (s *InboundService) DelInbound(id int) (bool, error) { db := database.GetDB() var tag string needRestart := false result := db.Model(model.Inbound{}).Select("tag").Where("id = ? and enable = ?", id, true).First(&tag) if result.Error == nil { s.xrayApi.Init(p.GetAPIPort()) err1 := s.xrayApi.DelInbound(tag) if err1 == nil { logger.Debug("Inbound deleted by api:", tag) } else { logger.Debug("Unable to delete inbound by api:", err1) needRestart = true } s.xrayApi.Close() } else { logger.Debug("No enabled inbound founded to removing by api", tag) } // Delete client traffics of inbounds err := db.Where("inbound_id = ?", id).Delete(xray.ClientTraffic{}).Error if err != nil { return false, err } inbound, err := s.GetInbound(id) if err != nil { return false, err } clients, err := s.GetClients(inbound) if err != nil { return false, err } for _, client := range clients { err := s.DelClientIPs(db, client.Email) if err != nil { return false, err } } return needRestart, db.Delete(model.Inbound{}, id).Error } func (s *InboundService) GetInbound(id int) (*model.Inbound, error) { db := database.GetDB() inbound := &model.Inbound{} err := db.Model(model.Inbound{}).First(inbound, id).Error if err != nil { return nil, err } return inbound, nil } // UpdateInbound modifies an existing inbound configuration. // It validates changes, updates the database, and syncs with the running Xray instance. // Returns the updated inbound, whether Xray needs restart, and any error. func (s *InboundService) UpdateInbound(inbound *model.Inbound) (*model.Inbound, bool, error) { exist, err := s.checkPortExist(inbound.Listen, inbound.Port, inbound.Id) if err != nil { return inbound, false, err } if exist { return inbound, false, common.NewError("Port already exists:", inbound.Port) } oldInbound, err := s.GetInbound(inbound.Id) if err != nil { return inbound, false, err } tag := oldInbound.Tag db := database.GetDB() tx := db.Begin() defer func() { if err != nil { tx.Rollback() } else { tx.Commit() } }() err = s.updateClientTraffics(tx, oldInbound, inbound) if err != nil { return inbound, false, err } // Ensure created_at and updated_at exist in inbound.Settings clients { var oldSettings map[string]any _ = json.Unmarshal([]byte(oldInbound.Settings), &oldSettings) emailToCreated := map[string]int64{} emailToUpdated := map[string]int64{} if oldSettings != nil { if oc, ok := oldSettings["clients"].([]any); ok { for _, it := range oc { if m, ok2 := it.(map[string]any); ok2 { if email, ok3 := m["email"].(string); ok3 { switch v := m["created_at"].(type) { case float64: emailToCreated[email] = int64(v) case int64: emailToCreated[email] = v } switch v := m["updated_at"].(type) { case float64: emailToUpdated[email] = int64(v) case int64: emailToUpdated[email] = v } } } } } } var newSettings map[string]any if err2 := json.Unmarshal([]byte(inbound.Settings), &newSettings); err2 == nil && newSettings != nil { now := time.Now().Unix() * 1000 if nSlice, ok := newSettings["clients"].([]any); ok { for i := range nSlice { if m, ok2 := nSlice[i].(map[string]any); ok2 { email, _ := m["email"].(string) if _, ok3 := m["created_at"]; !ok3 { if v, ok4 := emailToCreated[email]; ok4 && v > 0 { m["created_at"] = v } else { m["created_at"] = now } } // Preserve client's updated_at if present; do not bump on parent inbound update if _, hasUpdated := m["updated_at"]; !hasUpdated { if v, ok4 := emailToUpdated[email]; ok4 && v > 0 { m["updated_at"] = v } } nSlice[i] = m } } newSettings["clients"] = nSlice if bs, err3 := json.MarshalIndent(newSettings, "", " "); err3 == nil { inbound.Settings = string(bs) } } } } oldInbound.Up = inbound.Up oldInbound.Down = inbound.Down oldInbound.Total = inbound.Total oldInbound.Remark = inbound.Remark oldInbound.Enable = inbound.Enable oldInbound.ExpiryTime = inbound.ExpiryTime oldInbound.TrafficReset = inbound.TrafficReset oldInbound.Listen = inbound.Listen oldInbound.Port = inbound.Port oldInbound.Protocol = inbound.Protocol oldInbound.Settings = inbound.Settings oldInbound.StreamSettings = inbound.StreamSettings oldInbound.Sniffing = inbound.Sniffing if inbound.Listen == "" || inbound.Listen == "0.0.0.0" || inbound.Listen == "::" || inbound.Listen == "::0" { oldInbound.Tag = fmt.Sprintf("inbound-%v", inbound.Port) } else { oldInbound.Tag = fmt.Sprintf("inbound-%v:%v", inbound.Listen, inbound.Port) } needRestart := false s.xrayApi.Init(p.GetAPIPort()) if s.xrayApi.DelInbound(tag) == nil { logger.Debug("Old inbound deleted by api:", tag) } if inbound.Enable { inboundJson, err2 := json.MarshalIndent(oldInbound.GenXrayInboundConfig(), "", " ") if err2 != nil { logger.Debug("Unable to marshal updated inbound config:", err2) needRestart = true } else { err2 = s.xrayApi.AddInbound(inboundJson) if err2 == nil { logger.Debug("Updated inbound added by api:", oldInbound.Tag) } else { logger.Debug("Unable to update inbound by api:", err2) needRestart = true } } } s.xrayApi.Close() return inbound, needRestart, tx.Save(oldInbound).Error } func (s *InboundService) updateClientTraffics(tx *gorm.DB, oldInbound *model.Inbound, newInbound *model.Inbound) error { oldClients, err := s.GetClients(oldInbound) if err != nil { return err } newClients, err := s.GetClients(newInbound) if err != nil { return err } var emailExists bool for _, oldClient := range oldClients { emailExists = false for _, newClient := range newClients { if oldClient.Email == newClient.Email { emailExists = true break } } if !emailExists { err = s.DelClientStat(tx, oldClient.Email) if err != nil { return err } } } for _, newClient := range newClients { emailExists = false for _, oldClient := range oldClients { if newClient.Email == oldClient.Email { emailExists = true break } } if !emailExists { err = s.AddClientStat(tx, oldInbound.Id, &newClient) if err != nil { return err } } } return nil } func (s *InboundService) AddInboundClient(data *model.Inbound) (bool, error) { clients, err := s.GetClients(data) if err != nil { return false, err } var settings map[string]any err = json.Unmarshal([]byte(data.Settings), &settings) if err != nil { return false, err } interfaceClients := settings["clients"].([]any) // Add timestamps for new clients being appended nowTs := time.Now().Unix() * 1000 for i := range interfaceClients { if cm, ok := interfaceClients[i].(map[string]any); ok { if _, ok2 := cm["created_at"]; !ok2 { cm["created_at"] = nowTs } cm["updated_at"] = nowTs interfaceClients[i] = cm } } existEmail, err := s.checkEmailsExistForClients(clients) if err != nil { return false, err } if existEmail != "" { return false, common.NewError("Duplicate email:", existEmail) } oldInbound, err := s.GetInbound(data.Id) if err != nil { return false, err } // Secure client ID for _, client := range clients { switch oldInbound.Protocol { case "trojan": if client.Password == "" { return false, common.NewError("empty client ID") } case "shadowsocks": if client.Email == "" { return false, common.NewError("empty client ID") } default: if client.ID == "" { return false, common.NewError("empty client ID") } } } var oldSettings map[string]any err = json.Unmarshal([]byte(oldInbound.Settings), &oldSettings) if err != nil { return false, err } oldClients := oldSettings["clients"].([]any) oldClients = append(oldClients, interfaceClients...) oldSettings["clients"] = oldClients newSettings, err := json.MarshalIndent(oldSettings, "", " ") if err != nil { return false, err } oldInbound.Settings = string(newSettings) db := database.GetDB() tx := db.Begin() defer func() { if err != nil { tx.Rollback() } else { tx.Commit() } }() needRestart := false s.xrayApi.Init(p.GetAPIPort()) for _, client := range clients { if len(client.Email) > 0 { s.AddClientStat(tx, data.Id, &client) if client.Enable { cipher := "" if oldInbound.Protocol == "shadowsocks" { cipher = oldSettings["method"].(string) } err1 := s.xrayApi.AddUser(string(oldInbound.Protocol), oldInbound.Tag, map[string]any{ "email": client.Email, "id": client.ID, "security": client.Security, "flow": client.Flow, "password": client.Password, "cipher": cipher, }) if err1 == nil { logger.Debug("Client added by api:", client.Email) } else { logger.Debug("Error in adding client by api:", err1) needRestart = true } } } else { needRestart = true } } s.xrayApi.Close() return needRestart, tx.Save(oldInbound).Error } func (s *InboundService) DelInboundClient(inboundId int, clientId string) (bool, error) { oldInbound, err := s.GetInbound(inboundId) if err != nil { logger.Error("Load Old Data Error") return false, err } var settings map[string]any err = json.Unmarshal([]byte(oldInbound.Settings), &settings) if err != nil { return false, err } email := "" client_key := "id" if oldInbound.Protocol == "trojan" { client_key = "password" } if oldInbound.Protocol == "shadowsocks" { client_key = "email" } interfaceClients := settings["clients"].([]any) var newClients []any needApiDel := false for _, client := range interfaceClients { c := client.(map[string]any) c_id := c[client_key].(string) if c_id == clientId { email, _ = c["email"].(string) needApiDel, _ = c["enable"].(bool) } else { newClients = append(newClients, client) } } if len(newClients) == 0 { return false, common.NewError("no client remained in Inbound") } settings["clients"] = newClients newSettings, err := json.MarshalIndent(settings, "", " ") if err != nil { return false, err } oldInbound.Settings = string(newSettings) db := database.GetDB() err = s.DelClientIPs(db, email) if err != nil { logger.Error("Error in delete client IPs") return false, err } needRestart := false if len(email) > 0 { notDepleted := true err = db.Model(xray.ClientTraffic{}).Select("enable").Where("email = ?", email).First(¬Depleted).Error if err != nil { logger.Error("Get stats error") return false, err } err = s.DelClientStat(db, email) if err != nil { logger.Error("Delete stats Data Error") return false, err } if needApiDel && notDepleted { s.xrayApi.Init(p.GetAPIPort()) err1 := s.xrayApi.RemoveUser(oldInbound.Tag, email) if err1 == nil { logger.Debug("Client deleted by api:", email) needRestart = false } else { if strings.Contains(err1.Error(), fmt.Sprintf("User %s not found.", email)) { logger.Debug("User is already deleted. Nothing to do more...") } else { logger.Debug("Error in deleting client by api:", err1) needRestart = true } } s.xrayApi.Close() } } return needRestart, db.Save(oldInbound).Error } func (s *InboundService) UpdateInboundClient(data *model.Inbound, clientId string) (bool, error) { // TODO: check if TrafficReset field is updating clients, err := s.GetClients(data) if err != nil { return false, err } var settings map[string]any err = json.Unmarshal([]byte(data.Settings), &settings) if err != nil { return false, err } interfaceClients := settings["clients"].([]any) oldInbound, err := s.GetInbound(data.Id) if err != nil { return false, err } oldClients, err := s.GetClients(oldInbound) if err != nil { return false, err } oldEmail := "" newClientId := "" clientIndex := -1 for index, oldClient := range oldClients { oldClientId := "" switch oldInbound.Protocol { case "trojan": oldClientId = oldClient.Password newClientId = clients[0].Password case "shadowsocks": oldClientId = oldClient.Email newClientId = clients[0].Email default: oldClientId = oldClient.ID newClientId = clients[0].ID } if clientId == oldClientId { oldEmail = oldClient.Email clientIndex = index break } } // Validate new client ID if newClientId == "" || clientIndex == -1 { return false, common.NewError("empty client ID") } if len(clients[0].Email) > 0 && clients[0].Email != oldEmail { existEmail, err := s.checkEmailsExistForClients(clients) if err != nil { return false, err } if existEmail != "" { return false, common.NewError("Duplicate email:", existEmail) } } var oldSettings map[string]any err = json.Unmarshal([]byte(oldInbound.Settings), &oldSettings) if err != nil { return false, err } settingsClients := oldSettings["clients"].([]any) // Preserve created_at and set updated_at for the replacing client var preservedCreated any if clientIndex >= 0 && clientIndex < len(settingsClients) { if oldMap, ok := settingsClients[clientIndex].(map[string]any); ok { if v, ok2 := oldMap["created_at"]; ok2 { preservedCreated = v } } } if len(interfaceClients) > 0 { if newMap, ok := interfaceClients[0].(map[string]any); ok { if preservedCreated == nil { preservedCreated = time.Now().Unix() * 1000 } newMap["created_at"] = preservedCreated newMap["updated_at"] = time.Now().Unix() * 1000 interfaceClients[0] = newMap } } settingsClients[clientIndex] = interfaceClients[0] oldSettings["clients"] = settingsClients newSettings, err := json.MarshalIndent(oldSettings, "", " ") if err != nil { return false, err } oldInbound.Settings = string(newSettings) db := database.GetDB() tx := db.Begin() defer func() { if err != nil { tx.Rollback() } else { tx.Commit() } }() if len(clients[0].Email) > 0 { if len(oldEmail) > 0 { err = s.UpdateClientStat(tx, oldEmail, &clients[0]) if err != nil { return false, err } err = s.UpdateClientIPs(tx, oldEmail, clients[0].Email) if err != nil { return false, err } } else { s.AddClientStat(tx, data.Id, &clients[0]) } } else { err = s.DelClientStat(tx, oldEmail) if err != nil { return false, err } err = s.DelClientIPs(tx, oldEmail) if err != nil { return false, err } } needRestart := false if len(oldEmail) > 0 { s.xrayApi.Init(p.GetAPIPort()) if oldClients[clientIndex].Enable { err1 := s.xrayApi.RemoveUser(oldInbound.Tag, oldEmail) if err1 == nil { logger.Debug("Old client deleted by api:", oldEmail) } else { if strings.Contains(err1.Error(), fmt.Sprintf("User %s not found.", oldEmail)) { logger.Debug("User is already deleted. Nothing to do more...") } else { logger.Debug("Error in deleting client by api:", err1) needRestart = true } } } if clients[0].Enable { cipher := "" if oldInbound.Protocol == "shadowsocks" { cipher = oldSettings["method"].(string) } err1 := s.xrayApi.AddUser(string(oldInbound.Protocol), oldInbound.Tag, map[string]any{ "email": clients[0].Email, "id": clients[0].ID, "security": clients[0].Security, "flow": clients[0].Flow, "password": clients[0].Password, "cipher": cipher, }) if err1 == nil { logger.Debug("Client edited by api:", clients[0].Email) } else { logger.Debug("Error in adding client by api:", err1) needRestart = true } } s.xrayApi.Close() } else { logger.Debug("Client old email not found") needRestart = true } return needRestart, tx.Save(oldInbound).Error } func (s *InboundService) AddTraffic(inboundTraffics []*xray.Traffic, clientTraffics []*xray.ClientTraffic) (error, bool) { var err error db := database.GetDB() tx := db.Begin() defer func() { if err != nil { tx.Rollback() } else { tx.Commit() } }() err = s.addInboundTraffic(tx, inboundTraffics) if err != nil { return err, false } err = s.addClientTraffic(tx, clientTraffics) if err != nil { return err, false } needRestart0, count, err := s.autoRenewClients(tx) if err != nil { logger.Warning("Error in renew clients:", err) } else if count > 0 { logger.Debugf("%v clients renewed", count) } needRestart1, count, err := s.disableInvalidClients(tx) if err != nil { logger.Warning("Error in disabling invalid clients:", err) } else if count > 0 { logger.Debugf("%v clients disabled", count) } needRestart2, count, err := s.disableInvalidInbounds(tx) if err != nil { logger.Warning("Error in disabling invalid inbounds:", err) } else if count > 0 { logger.Debugf("%v inbounds disabled", count) } return nil, (needRestart0 || needRestart1 || needRestart2) } func (s *InboundService) addInboundTraffic(tx *gorm.DB, traffics []*xray.Traffic) error { if len(traffics) == 0 { return nil } var err error for _, traffic := range traffics { if traffic.IsInbound { err = tx.Model(&model.Inbound{}).Where("tag = ?", traffic.Tag). Updates(map[string]any{ "up": gorm.Expr("up + ?", traffic.Up), "down": gorm.Expr("down + ?", traffic.Down), "all_time": gorm.Expr("COALESCE(all_time, 0) + ?", traffic.Up+traffic.Down), }).Error if err != nil { return err } } } return nil } func (s *InboundService) addClientTraffic(tx *gorm.DB, traffics []*xray.ClientTraffic) (err error) { if len(traffics) == 0 { // Empty onlineUsers if p != nil { p.SetOnlineClients(make([]string, 0)) } return nil } onlineClients := make([]string, 0) emails := make([]string, 0, len(traffics)) for _, traffic := range traffics { emails = append(emails, traffic.Email) } dbClientTraffics := make([]*xray.ClientTraffic, 0, len(traffics)) err = tx.Model(xray.ClientTraffic{}).Where("email IN (?)", emails).Find(&dbClientTraffics).Error if err != nil { return err } // Avoid empty slice error if len(dbClientTraffics) == 0 { return nil } dbClientTraffics, err = s.adjustTraffics(tx, dbClientTraffics) if err != nil { return err } for dbTraffic_index := range dbClientTraffics { for traffic_index := range traffics { if dbClientTraffics[dbTraffic_index].Email == traffics[traffic_index].Email { dbClientTraffics[dbTraffic_index].Up += traffics[traffic_index].Up dbClientTraffics[dbTraffic_index].Down += traffics[traffic_index].Down dbClientTraffics[dbTraffic_index].AllTime += (traffics[traffic_index].Up + traffics[traffic_index].Down) // Add user in onlineUsers array on traffic if traffics[traffic_index].Up+traffics[traffic_index].Down > 0 { onlineClients = append(onlineClients, traffics[traffic_index].Email) dbClientTraffics[dbTraffic_index].LastOnline = time.Now().UnixMilli() } break } } } // Set onlineUsers p.SetOnlineClients(onlineClients) err = tx.Save(dbClientTraffics).Error if err != nil { logger.Warning("AddClientTraffic update data ", err) } return nil } func (s *InboundService) adjustTraffics(tx *gorm.DB, dbClientTraffics []*xray.ClientTraffic) ([]*xray.ClientTraffic, error) { inboundIds := make([]int, 0, len(dbClientTraffics)) for _, dbClientTraffic := range dbClientTraffics { if dbClientTraffic.ExpiryTime < 0 { inboundIds = append(inboundIds, dbClientTraffic.InboundId) } } if len(inboundIds) > 0 { var inbounds []*model.Inbound err := tx.Model(model.Inbound{}).Where("id IN (?)", inboundIds).Find(&inbounds).Error if err != nil { return nil, err } for inbound_index := range inbounds { settings := map[string]any{} json.Unmarshal([]byte(inbounds[inbound_index].Settings), &settings) clients, ok := settings["clients"].([]any) if ok { var newClients []any for client_index := range clients { c := clients[client_index].(map[string]any) for traffic_index := range dbClientTraffics { if dbClientTraffics[traffic_index].ExpiryTime < 0 && c["email"] == dbClientTraffics[traffic_index].Email { oldExpiryTime := c["expiryTime"].(float64) newExpiryTime := (time.Now().Unix() * 1000) - int64(oldExpiryTime) c["expiryTime"] = newExpiryTime c["updated_at"] = time.Now().Unix() * 1000 dbClientTraffics[traffic_index].ExpiryTime = newExpiryTime break } } // Backfill created_at and updated_at if _, ok := c["created_at"]; !ok { c["created_at"] = time.Now().Unix() * 1000 } c["updated_at"] = time.Now().Unix() * 1000 newClients = append(newClients, any(c)) } settings["clients"] = newClients modifiedSettings, err := json.MarshalIndent(settings, "", " ") if err != nil { return nil, err } inbounds[inbound_index].Settings = string(modifiedSettings) } } err = tx.Save(inbounds).Error if err != nil { logger.Warning("AddClientTraffic update inbounds ", err) logger.Error(inbounds) } } return dbClientTraffics, nil } func (s *InboundService) autoRenewClients(tx *gorm.DB) (bool, int64, error) { // check for time expired var traffics []*xray.ClientTraffic now := time.Now().Unix() * 1000 var err, err1 error err = tx.Model(xray.ClientTraffic{}).Where("reset > 0 and expiry_time > 0 and expiry_time <= ?", now).Find(&traffics).Error if err != nil { return false, 0, err } // return if there is no client to renew if len(traffics) == 0 { return false, 0, nil } var inbound_ids []int var inbounds []*model.Inbound needRestart := false var clientsToAdd []struct { protocol string tag string client map[string]any } for _, traffic := range traffics { inbound_ids = append(inbound_ids, traffic.InboundId) } err = tx.Model(model.Inbound{}).Where("id IN ?", inbound_ids).Find(&inbounds).Error if err != nil { return false, 0, err } for inbound_index := range inbounds { settings := map[string]any{} json.Unmarshal([]byte(inbounds[inbound_index].Settings), &settings) clients := settings["clients"].([]any) for client_index := range clients { c := clients[client_index].(map[string]any) for traffic_index, traffic := range traffics { if traffic.Email == c["email"].(string) { newExpiryTime := traffic.ExpiryTime for newExpiryTime < now { newExpiryTime += (int64(traffic.Reset) * 86400000) } c["expiryTime"] = newExpiryTime traffics[traffic_index].ExpiryTime = newExpiryTime traffics[traffic_index].Down = 0 traffics[traffic_index].Up = 0 if !traffic.Enable { traffics[traffic_index].Enable = true clientsToAdd = append(clientsToAdd, struct { protocol string tag string client map[string]any }{ protocol: string(inbounds[inbound_index].Protocol), tag: inbounds[inbound_index].Tag, client: c, }) } clients[client_index] = any(c) break } } } settings["clients"] = clients newSettings, err := json.MarshalIndent(settings, "", " ") if err != nil { return false, 0, err } inbounds[inbound_index].Settings = string(newSettings) } err = tx.Save(inbounds).Error if err != nil { return false, 0, err } err = tx.Save(traffics).Error if err != nil { return false, 0, err } if p != nil { err1 = s.xrayApi.Init(p.GetAPIPort()) if err1 != nil { return true, int64(len(traffics)), nil } for _, clientToAdd := range clientsToAdd { err1 = s.xrayApi.AddUser(clientToAdd.protocol, clientToAdd.tag, clientToAdd.client) if err1 != nil { needRestart = true } } s.xrayApi.Close() } return needRestart, int64(len(traffics)), nil } func (s *InboundService) disableInvalidInbounds(tx *gorm.DB) (bool, int64, error) { now := time.Now().Unix() * 1000 needRestart := false if p != nil { var tags []string err := tx.Table("inbounds"). Select("inbounds.tag"). Where("((total > 0 and up + down >= total) or (expiry_time > 0 and expiry_time <= ?)) and enable = ?", now, true). Scan(&tags).Error if err != nil { return false, 0, err } s.xrayApi.Init(p.GetAPIPort()) for _, tag := range tags { err1 := s.xrayApi.DelInbound(tag) if err1 == nil { logger.Debug("Inbound disabled by api:", tag) } else { logger.Debug("Error in disabling inbound by api:", err1) needRestart = true } } s.xrayApi.Close() } result := tx.Model(model.Inbound{}). Where("((total > 0 and up + down >= total) or (expiry_time > 0 and expiry_time <= ?)) and enable = ?", now, true). Update("enable", false) err := result.Error count := result.RowsAffected return needRestart, count, err } func (s *InboundService) disableInvalidClients(tx *gorm.DB) (bool, int64, error) { now := time.Now().Unix() * 1000 needRestart := false if p != nil { var results []struct { Tag string Email string } err := tx.Table("inbounds"). Select("inbounds.tag, client_traffics.email"). Joins("JOIN client_traffics ON inbounds.id = client_traffics.inbound_id"). Where("((client_traffics.total > 0 AND client_traffics.up + client_traffics.down >= client_traffics.total) OR (client_traffics.expiry_time > 0 AND client_traffics.expiry_time <= ?)) AND client_traffics.enable = ?", now, true). Scan(&results).Error if err != nil { return false, 0, err } s.xrayApi.Init(p.GetAPIPort()) for _, result := range results { err1 := s.xrayApi.RemoveUser(result.Tag, result.Email) if err1 == nil { logger.Debug("Client disabled by api:", result.Email) } else { if strings.Contains(err1.Error(), fmt.Sprintf("User %s not found.", result.Email)) { logger.Debug("User is already disabled. Nothing to do more...") } else { if strings.Contains(err1.Error(), fmt.Sprintf("User %s not found.", result.Email)) { logger.Debug("User is already disabled. Nothing to do more...") } else { logger.Debug("Error in disabling client by api:", err1) needRestart = true } } } } s.xrayApi.Close() } result := tx.Model(xray.ClientTraffic{}). Where("((total > 0 and up + down >= total) or (expiry_time > 0 and expiry_time <= ?)) and enable = ?", now, true). Update("enable", false) err := result.Error count := result.RowsAffected return needRestart, count, err } func (s *InboundService) GetInboundTags() (string, error) { db := database.GetDB() var inboundTags []string err := db.Model(model.Inbound{}).Select("tag").Find(&inboundTags).Error if err != nil && err != gorm.ErrRecordNotFound { return "", err } tags, _ := json.Marshal(inboundTags) return string(tags), nil } func (s *InboundService) MigrationRemoveOrphanedTraffics() { db := database.GetDB() db.Exec(` DELETE FROM client_traffics WHERE email NOT IN ( SELECT JSON_EXTRACT(client.value, '$.email') FROM inbounds, JSON_EACH(JSON_EXTRACT(inbounds.settings, '$.clients')) AS client ) `) } func (s *InboundService) AddClientStat(tx *gorm.DB, inboundId int, client *model.Client) error { clientTraffic := xray.ClientTraffic{} clientTraffic.InboundId = inboundId clientTraffic.Email = client.Email clientTraffic.Total = client.TotalGB clientTraffic.ExpiryTime = client.ExpiryTime clientTraffic.Enable = client.Enable clientTraffic.Up = 0 clientTraffic.Down = 0 clientTraffic.Reset = client.Reset result := tx.Create(&clientTraffic) err := result.Error return err } func (s *InboundService) UpdateClientStat(tx *gorm.DB, email string, client *model.Client) error { result := tx.Model(xray.ClientTraffic{}). Where("email = ?", email). Updates(map[string]any{ "enable": client.Enable, "email": client.Email, "total": client.TotalGB, "expiry_time": client.ExpiryTime, "reset": client.Reset, }) err := result.Error return err } func (s *InboundService) UpdateClientIPs(tx *gorm.DB, oldEmail string, newEmail string) error { return tx.Model(model.InboundClientIps{}).Where("client_email = ?", oldEmail).Update("client_email", newEmail).Error } func (s *InboundService) DelClientStat(tx *gorm.DB, email string) error { return tx.Where("email = ?", email).Delete(xray.ClientTraffic{}).Error } func (s *InboundService) DelClientIPs(tx *gorm.DB, email string) error { return tx.Where("client_email = ?", email).Delete(model.InboundClientIps{}).Error } func (s *InboundService) GetClientInboundByTrafficID(trafficId int) (traffic *xray.ClientTraffic, inbound *model.Inbound, err error) { db := database.GetDB() var traffics []*xray.ClientTraffic err = db.Model(xray.ClientTraffic{}).Where("id = ?", trafficId).Find(&traffics).Error if err != nil { logger.Warningf("Error retrieving ClientTraffic with trafficId %d: %v", trafficId, err) return nil, nil, err } if len(traffics) > 0 { inbound, err = s.GetInbound(traffics[0].InboundId) return traffics[0], inbound, err } return nil, nil, nil } func (s *InboundService) GetClientInboundByEmail(email string) (traffic *xray.ClientTraffic, inbound *model.Inbound, err error) { db := database.GetDB() var traffics []*xray.ClientTraffic err = db.Model(xray.ClientTraffic{}).Where("email = ?", email).Find(&traffics).Error if err != nil { logger.Warningf("Error retrieving ClientTraffic with email %s: %v", email, err) return nil, nil, err } if len(traffics) > 0 { inbound, err = s.GetInbound(traffics[0].InboundId) return traffics[0], inbound, err } return nil, nil, nil } func (s *InboundService) GetClientByEmail(clientEmail string) (*xray.ClientTraffic, *model.Client, error) { traffic, inbound, err := s.GetClientInboundByEmail(clientEmail) if err != nil { return nil, nil, err } if inbound == nil { return nil, nil, common.NewError("Inbound Not Found For Email:", clientEmail) } clients, err := s.GetClients(inbound) if err != nil { return nil, nil, err } for _, client := range clients { if client.Email == clientEmail { return traffic, &client, nil } } return nil, nil, common.NewError("Client Not Found In Inbound For Email:", clientEmail) } func (s *InboundService) SetClientTelegramUserID(trafficId int, tgId int64) (bool, error) { traffic, inbound, err := s.GetClientInboundByTrafficID(trafficId) if err != nil { return false, err } if inbound == nil { return false, common.NewError("Inbound Not Found For Traffic ID:", trafficId) } clientEmail := traffic.Email oldClients, err := s.GetClients(inbound) if err != nil { return false, err } clientId := "" for _, oldClient := range oldClients { if oldClient.Email == clientEmail { switch inbound.Protocol { case "trojan": clientId = oldClient.Password case "shadowsocks": clientId = oldClient.Email default: clientId = oldClient.ID } break } } if len(clientId) == 0 { return false, common.NewError("Client Not Found For Email:", clientEmail) } var settings map[string]any err = json.Unmarshal([]byte(inbound.Settings), &settings) if err != nil { return false, err } clients := settings["clients"].([]any) var newClients []any for client_index := range clients { c := clients[client_index].(map[string]any) if c["email"] == clientEmail { c["tgId"] = tgId c["updated_at"] = time.Now().Unix() * 1000 newClients = append(newClients, any(c)) } } settings["clients"] = newClients modifiedSettings, err := json.MarshalIndent(settings, "", " ") if err != nil { return false, err } inbound.Settings = string(modifiedSettings) needRestart, err := s.UpdateInboundClient(inbound, clientId) return needRestart, err } func (s *InboundService) checkIsEnabledByEmail(clientEmail string) (bool, error) { _, inbound, err := s.GetClientInboundByEmail(clientEmail) if err != nil { return false, err } if inbound == nil { return false, common.NewError("Inbound Not Found For Email:", clientEmail) } clients, err := s.GetClients(inbound) if err != nil { return false, err } isEnable := false for _, client := range clients { if client.Email == clientEmail { isEnable = client.Enable break } } return isEnable, err } func (s *InboundService) ToggleClientEnableByEmail(clientEmail string) (bool, bool, error) { _, inbound, err := s.GetClientInboundByEmail(clientEmail) if err != nil { return false, false, err } if inbound == nil { return false, false, common.NewError("Inbound Not Found For Email:", clientEmail) } oldClients, err := s.GetClients(inbound) if err != nil { return false, false, err } clientId := "" clientOldEnabled := false for _, oldClient := range oldClients { if oldClient.Email == clientEmail { switch inbound.Protocol { case "trojan": clientId = oldClient.Password case "shadowsocks": clientId = oldClient.Email default: clientId = oldClient.ID } clientOldEnabled = oldClient.Enable break } } if len(clientId) == 0 { return false, false, common.NewError("Client Not Found For Email:", clientEmail) } var settings map[string]any err = json.Unmarshal([]byte(inbound.Settings), &settings) if err != nil { return false, false, err } clients := settings["clients"].([]any) var newClients []any for client_index := range clients { c := clients[client_index].(map[string]any) if c["email"] == clientEmail { c["enable"] = !clientOldEnabled c["updated_at"] = time.Now().Unix() * 1000 newClients = append(newClients, any(c)) } } settings["clients"] = newClients modifiedSettings, err := json.MarshalIndent(settings, "", " ") if err != nil { return false, false, err } inbound.Settings = string(modifiedSettings) needRestart, err := s.UpdateInboundClient(inbound, clientId) if err != nil { return false, needRestart, err } return !clientOldEnabled, needRestart, nil } // SetClientEnableByEmail sets client enable state to desired value; returns (changed, needRestart, error) func (s *InboundService) SetClientEnableByEmail(clientEmail string, enable bool) (bool, bool, error) { current, err := s.checkIsEnabledByEmail(clientEmail) if err != nil { return false, false, err } if current == enable { return false, false, nil } newEnabled, needRestart, err := s.ToggleClientEnableByEmail(clientEmail) if err != nil { return false, needRestart, err } return newEnabled == enable, needRestart, nil } func (s *InboundService) ResetClientIpLimitByEmail(clientEmail string, count int) (bool, error) { _, inbound, err := s.GetClientInboundByEmail(clientEmail) if err != nil { return false, err } if inbound == nil { return false, common.NewError("Inbound Not Found For Email:", clientEmail) } oldClients, err := s.GetClients(inbound) if err != nil { return false, err } clientId := "" for _, oldClient := range oldClients { if oldClient.Email == clientEmail { switch inbound.Protocol { case "trojan": clientId = oldClient.Password case "shadowsocks": clientId = oldClient.Email default: clientId = oldClient.ID } break } } if len(clientId) == 0 { return false, common.NewError("Client Not Found For Email:", clientEmail) } var settings map[string]any err = json.Unmarshal([]byte(inbound.Settings), &settings) if err != nil { return false, err } clients := settings["clients"].([]any) var newClients []any for client_index := range clients { c := clients[client_index].(map[string]any) if c["email"] == clientEmail { c["limitIp"] = count c["updated_at"] = time.Now().Unix() * 1000 newClients = append(newClients, any(c)) } } settings["clients"] = newClients modifiedSettings, err := json.MarshalIndent(settings, "", " ") if err != nil { return false, err } inbound.Settings = string(modifiedSettings) needRestart, err := s.UpdateInboundClient(inbound, clientId) return needRestart, err } func (s *InboundService) ResetClientExpiryTimeByEmail(clientEmail string, expiry_time int64) (bool, error) { _, inbound, err := s.GetClientInboundByEmail(clientEmail) if err != nil { return false, err } if inbound == nil { return false, common.NewError("Inbound Not Found For Email:", clientEmail) } oldClients, err := s.GetClients(inbound) if err != nil { return false, err } clientId := "" for _, oldClient := range oldClients { if oldClient.Email == clientEmail { switch inbound.Protocol { case "trojan": clientId = oldClient.Password case "shadowsocks": clientId = oldClient.Email default: clientId = oldClient.ID } break } } if len(clientId) == 0 { return false, common.NewError("Client Not Found For Email:", clientEmail) } var settings map[string]any err = json.Unmarshal([]byte(inbound.Settings), &settings) if err != nil { return false, err } clients := settings["clients"].([]any) var newClients []any for client_index := range clients { c := clients[client_index].(map[string]any) if c["email"] == clientEmail { c["expiryTime"] = expiry_time c["updated_at"] = time.Now().Unix() * 1000 newClients = append(newClients, any(c)) } } settings["clients"] = newClients modifiedSettings, err := json.MarshalIndent(settings, "", " ") if err != nil { return false, err } inbound.Settings = string(modifiedSettings) needRestart, err := s.UpdateInboundClient(inbound, clientId) return needRestart, err } func (s *InboundService) ResetClientTrafficLimitByEmail(clientEmail string, totalGB int) (bool, error) { if totalGB < 0 { return false, common.NewError("totalGB must be >= 0") } _, inbound, err := s.GetClientInboundByEmail(clientEmail) if err != nil { return false, err } if inbound == nil { return false, common.NewError("Inbound Not Found For Email:", clientEmail) } oldClients, err := s.GetClients(inbound) if err != nil { return false, err } clientId := "" for _, oldClient := range oldClients { if oldClient.Email == clientEmail { switch inbound.Protocol { case "trojan": clientId = oldClient.Password case "shadowsocks": clientId = oldClient.Email default: clientId = oldClient.ID } break } } if len(clientId) == 0 { return false, common.NewError("Client Not Found For Email:", clientEmail) } var settings map[string]any err = json.Unmarshal([]byte(inbound.Settings), &settings) if err != nil { return false, err } clients := settings["clients"].([]any) var newClients []any for client_index := range clients { c := clients[client_index].(map[string]any) if c["email"] == clientEmail { c["totalGB"] = totalGB * 1024 * 1024 * 1024 c["updated_at"] = time.Now().Unix() * 1000 newClients = append(newClients, any(c)) } } settings["clients"] = newClients modifiedSettings, err := json.MarshalIndent(settings, "", " ") if err != nil { return false, err } inbound.Settings = string(modifiedSettings) needRestart, err := s.UpdateInboundClient(inbound, clientId) return needRestart, err } func (s *InboundService) ResetClientTrafficByEmail(clientEmail string) error { db := database.GetDB() // Reset traffic stats in ClientTraffic table result := db.Model(xray.ClientTraffic{}). Where("email = ?", clientEmail). Updates(map[string]any{"enable": true, "up": 0, "down": 0}) err := result.Error if err != nil { return err } return nil } func (s *InboundService) ResetClientTraffic(id int, clientEmail string) (bool, error) { needRestart := false traffic, err := s.GetClientTrafficByEmail(clientEmail) if err != nil { return false, err } if !traffic.Enable { inbound, err := s.GetInbound(id) if err != nil { return false, err } clients, err := s.GetClients(inbound) if err != nil { return false, err } for _, client := range clients { if client.Email == clientEmail && client.Enable { s.xrayApi.Init(p.GetAPIPort()) cipher := "" if string(inbound.Protocol) == "shadowsocks" { var oldSettings map[string]any err = json.Unmarshal([]byte(inbound.Settings), &oldSettings) if err != nil { return false, err } cipher = oldSettings["method"].(string) } err1 := s.xrayApi.AddUser(string(inbound.Protocol), inbound.Tag, map[string]any{ "email": client.Email, "id": client.ID, "security": client.Security, "flow": client.Flow, "password": client.Password, "cipher": cipher, }) if err1 == nil { logger.Debug("Client enabled due to reset traffic:", clientEmail) } else { logger.Debug("Error in enabling client by api:", err1) needRestart = true } s.xrayApi.Close() break } } } traffic.Up = 0 traffic.Down = 0 traffic.Enable = true db := database.GetDB() err = db.Save(traffic).Error if err != nil { return false, err } return needRestart, nil } func (s *InboundService) ResetAllClientTraffics(id int) error { db := database.GetDB() now := time.Now().Unix() * 1000 return db.Transaction(func(tx *gorm.DB) error { whereText := "inbound_id " if id == -1 { whereText += " > ?" } else { whereText += " = ?" } // Reset client traffics result := tx.Model(xray.ClientTraffic{}). Where(whereText, id). Updates(map[string]any{"enable": true, "up": 0, "down": 0}) if result.Error != nil { return result.Error } // Update lastTrafficResetTime for the inbound(s) inboundWhereText := "id " if id == -1 { inboundWhereText += " > ?" } else { inboundWhereText += " = ?" } result = tx.Model(model.Inbound{}). Where(inboundWhereText, id). Update("last_traffic_reset_time", now) return result.Error }) } func (s *InboundService) ResetAllTraffics() error { db := database.GetDB() result := db.Model(model.Inbound{}). Where("user_id > ?", 0). Updates(map[string]any{"up": 0, "down": 0}) err := result.Error return err } func (s *InboundService) DelDepletedClients(id int) (err error) { db := database.GetDB() tx := db.Begin() defer func() { if err == nil { tx.Commit() } else { tx.Rollback() } }() whereText := "reset = 0 and inbound_id " if id < 0 { whereText += "> ?" } else { whereText += "= ?" } // Only consider truly depleted clients: expired OR traffic exhausted now := time.Now().Unix() * 1000 depletedClients := []xray.ClientTraffic{} err = db.Model(xray.ClientTraffic{}). Where(whereText+" and ((total > 0 and up + down >= total) or (expiry_time > 0 and expiry_time <= ?))", id, now). Select("inbound_id, GROUP_CONCAT(email) as email"). Group("inbound_id"). Find(&depletedClients).Error if err != nil { return err } for _, depletedClient := range depletedClients { emails := strings.Split(depletedClient.Email, ",") oldInbound, err := s.GetInbound(depletedClient.InboundId) if err != nil { return err } var oldSettings map[string]any err = json.Unmarshal([]byte(oldInbound.Settings), &oldSettings) if err != nil { return err } oldClients := oldSettings["clients"].([]any) var newClients []any for _, client := range oldClients { deplete := false c := client.(map[string]any) for _, email := range emails { if email == c["email"].(string) { deplete = true break } } if !deplete { newClients = append(newClients, client) } } if len(newClients) > 0 { oldSettings["clients"] = newClients newSettings, err := json.MarshalIndent(oldSettings, "", " ") if err != nil { return err } oldInbound.Settings = string(newSettings) err = tx.Save(oldInbound).Error if err != nil { return err } } else { // Delete inbound if no client remains s.DelInbound(depletedClient.InboundId) } } // Delete stats only for truly depleted clients err = tx.Where(whereText+" and ((total > 0 and up + down >= total) or (expiry_time > 0 and expiry_time <= ?))", id, now).Delete(xray.ClientTraffic{}).Error if err != nil { return err } return nil } func (s *InboundService) GetClientTrafficTgBot(tgId int64) ([]*xray.ClientTraffic, error) { db := database.GetDB() var inbounds []*model.Inbound // Retrieve inbounds where settings contain the given tgId err := db.Model(model.Inbound{}).Where("settings LIKE ?", fmt.Sprintf(`%%"tgId": %d%%`, tgId)).Find(&inbounds).Error if err != nil && err != gorm.ErrRecordNotFound { logger.Errorf("Error retrieving inbounds with tgId %d: %v", tgId, err) return nil, err } var emails []string for _, inbound := range inbounds { clients, err := s.GetClients(inbound) if err != nil { logger.Errorf("Error retrieving clients for inbound %d: %v", inbound.Id, err) continue } for _, client := range clients { if client.TgID == tgId { emails = append(emails, client.Email) } } } var traffics []*xray.ClientTraffic err = db.Model(xray.ClientTraffic{}).Where("email IN ?", emails).Find(&traffics).Error if err != nil { if err == gorm.ErrRecordNotFound { logger.Warning("No ClientTraffic records found for emails:", emails) return nil, nil } logger.Errorf("Error retrieving ClientTraffic for emails %v: %v", emails, err) return nil, err } // Populate UUID and other client data for each traffic record for i := range traffics { if ct, client, e := s.GetClientByEmail(traffics[i].Email); e == nil && ct != nil && client != nil { traffics[i].Enable = client.Enable traffics[i].UUID = client.ID traffics[i].SubId = client.SubID } } return traffics, nil } func (s *InboundService) GetClientTrafficByEmail(email string) (traffic *xray.ClientTraffic, err error) { // Prefer retrieving along with client to reflect actual enabled state from inbound settings t, client, err := s.GetClientByEmail(email) if err != nil { logger.Warningf("Error retrieving ClientTraffic with email %s: %v", email, err) return nil, err } if t != nil && client != nil { t.UUID = client.ID t.SubId = client.SubID return t, nil } return nil, nil } func (s *InboundService) UpdateClientTrafficByEmail(email string, upload int64, download int64) error { db := database.GetDB() result := db.Model(xray.ClientTraffic{}). Where("email = ?", email). Updates(map[string]any{"up": upload, "down": download}) err := result.Error if err != nil { logger.Warningf("Error updating ClientTraffic with email %s: %v", email, err) return err } return nil } func (s *InboundService) GetClientTrafficByID(id string) ([]xray.ClientTraffic, error) { db := database.GetDB() var traffics []xray.ClientTraffic err := db.Model(xray.ClientTraffic{}).Where(`email IN( SELECT JSON_EXTRACT(client.value, '$.email') as email FROM inbounds, JSON_EACH(JSON_EXTRACT(inbounds.settings, '$.clients')) AS client WHERE JSON_EXTRACT(client.value, '$.id') in (?) )`, id).Find(&traffics).Error if err != nil { logger.Debug(err) return nil, err } // Reconcile enable flag with client settings per email to avoid stale DB value for i := range traffics { if ct, client, e := s.GetClientByEmail(traffics[i].Email); e == nil && ct != nil && client != nil { traffics[i].Enable = client.Enable traffics[i].UUID = client.ID traffics[i].SubId = client.SubID } } return traffics, err } func (s *InboundService) SearchClientTraffic(query string) (traffic *xray.ClientTraffic, err error) { db := database.GetDB() inbound := &model.Inbound{} traffic = &xray.ClientTraffic{} // Search for inbound settings that contain the query err = db.Model(model.Inbound{}).Where("settings LIKE ?", "%\""+query+"\"%").First(inbound).Error if err != nil { if err == gorm.ErrRecordNotFound { logger.Warningf("Inbound settings containing query %s not found: %v", query, err) return nil, err } logger.Errorf("Error searching for inbound settings with query %s: %v", query, err) return nil, err } traffic.InboundId = inbound.Id // Unmarshal settings to get clients settings := map[string][]model.Client{} if err := json.Unmarshal([]byte(inbound.Settings), &settings); err != nil { logger.Errorf("Error unmarshalling inbound settings for inbound ID %d: %v", inbound.Id, err) return nil, err } clients := settings["clients"] for _, client := range clients { if (client.ID == query || client.Password == query) && client.Email != "" { traffic.Email = client.Email break } } if traffic.Email == "" { logger.Warningf("No client found with query %s in inbound ID %d", query, inbound.Id) return nil, gorm.ErrRecordNotFound } // Retrieve ClientTraffic based on the found email err = db.Model(xray.ClientTraffic{}).Where("email = ?", traffic.Email).First(traffic).Error if err != nil { if err == gorm.ErrRecordNotFound { logger.Warningf("ClientTraffic for email %s not found: %v", traffic.Email, err) return nil, err } logger.Errorf("Error retrieving ClientTraffic for email %s: %v", traffic.Email, err) return nil, err } return traffic, nil } func (s *InboundService) GetInboundClientIps(clientEmail string) (string, error) { db := database.GetDB() InboundClientIps := &model.InboundClientIps{} err := db.Model(model.InboundClientIps{}).Where("client_email = ?", clientEmail).First(InboundClientIps).Error if err != nil { return "", err } if InboundClientIps.Ips == "" { return "", nil } // Try to parse as new format (with timestamps) type IPWithTimestamp struct { IP string `json:"ip"` Timestamp int64 `json:"timestamp"` } var ipsWithTime []IPWithTimestamp err = json.Unmarshal([]byte(InboundClientIps.Ips), &ipsWithTime) // If successfully parsed as new format, return with timestamps if err == nil && len(ipsWithTime) > 0 { return InboundClientIps.Ips, nil } // Otherwise, assume it's old format (simple string array) // Try to parse as simple array and convert to new format var oldIps []string err = json.Unmarshal([]byte(InboundClientIps.Ips), &oldIps) if err == nil && len(oldIps) > 0 { // Convert old format to new format with current timestamp newIpsWithTime := make([]IPWithTimestamp, len(oldIps)) for i, ip := range oldIps { newIpsWithTime[i] = IPWithTimestamp{ IP: ip, Timestamp: time.Now().Unix(), } } result, _ := json.Marshal(newIpsWithTime) return string(result), nil } // Return as-is if parsing fails return InboundClientIps.Ips, nil } func (s *InboundService) ClearClientIps(clientEmail string) error { db := database.GetDB() result := db.Model(model.InboundClientIps{}). Where("client_email = ?", clientEmail). Update("ips", "") err := result.Error if err != nil { return err } return nil } func (s *InboundService) SearchInbounds(query string) ([]*model.Inbound, error) { db := database.GetDB() var inbounds []*model.Inbound err := db.Model(model.Inbound{}).Preload("ClientStats").Where("remark like ?", "%"+query+"%").Find(&inbounds).Error if err != nil && err != gorm.ErrRecordNotFound { return nil, err } return inbounds, nil } func (s *InboundService) MigrationRequirements() { db := database.GetDB() tx := db.Begin() var err error defer func() { if err == nil { tx.Commit() if dbErr := db.Exec(`VACUUM "main"`).Error; dbErr != nil { logger.Warningf("VACUUM failed: %v", dbErr) } } else { tx.Rollback() } }() // Calculate and backfill all_time from up+down for inbounds and clients err = tx.Exec(` UPDATE inbounds SET all_time = IFNULL(up, 0) + IFNULL(down, 0) WHERE IFNULL(all_time, 0) = 0 AND (IFNULL(up, 0) + IFNULL(down, 0)) > 0 `).Error if err != nil { return } err = tx.Exec(` UPDATE client_traffics SET all_time = IFNULL(up, 0) + IFNULL(down, 0) WHERE IFNULL(all_time, 0) = 0 AND (IFNULL(up, 0) + IFNULL(down, 0)) > 0 `).Error if err != nil { return } // Fix inbounds based problems var inbounds []*model.Inbound err = tx.Model(model.Inbound{}).Where("protocol IN (?)", []string{"vmess", "vless", "trojan"}).Find(&inbounds).Error if err != nil && err != gorm.ErrRecordNotFound { return } for inbound_index := range inbounds { settings := map[string]any{} json.Unmarshal([]byte(inbounds[inbound_index].Settings), &settings) clients, ok := settings["clients"].([]any) if ok { // Fix Client configuration problems var newClients []any for client_index := range clients { c := clients[client_index].(map[string]any) // Add email='' if it is not exists if _, ok := c["email"]; !ok { c["email"] = "" } // Convert string tgId to int64 if _, ok := c["tgId"]; ok { var tgId any = c["tgId"] if tgIdStr, ok2 := tgId.(string); ok2 { tgIdInt64, err := strconv.ParseInt(strings.ReplaceAll(tgIdStr, " ", ""), 10, 64) if err == nil { c["tgId"] = tgIdInt64 } } } // Remove "flow": "xtls-rprx-direct" if _, ok := c["flow"]; ok { if c["flow"] == "xtls-rprx-direct" { c["flow"] = "" } } // Backfill created_at and updated_at if _, ok := c["created_at"]; !ok { c["created_at"] = time.Now().Unix() * 1000 } c["updated_at"] = time.Now().Unix() * 1000 newClients = append(newClients, any(c)) } settings["clients"] = newClients modifiedSettings, err := json.MarshalIndent(settings, "", " ") if err != nil { return } inbounds[inbound_index].Settings = string(modifiedSettings) } // Add client traffic row for all clients which has email modelClients, err := s.GetClients(inbounds[inbound_index]) if err != nil { return } for _, modelClient := range modelClients { if len(modelClient.Email) > 0 { var count int64 tx.Model(xray.ClientTraffic{}).Where("email = ?", modelClient.Email).Count(&count) if count == 0 { s.AddClientStat(tx, inbounds[inbound_index].Id, &modelClient) } } } } tx.Save(inbounds) // Remove orphaned traffics tx.Where("inbound_id = 0").Delete(xray.ClientTraffic{}) // Migrate old MultiDomain to External Proxy var externalProxy []struct { Id int Port int StreamSettings []byte } err = tx.Raw(`select id, port, stream_settings from inbounds WHERE protocol in ('vmess','vless','trojan') AND json_extract(stream_settings, '$.security') = 'tls' AND json_extract(stream_settings, '$.tlsSettings.settings.domains') IS NOT NULL`).Scan(&externalProxy).Error if err != nil || len(externalProxy) == 0 { return } for _, ep := range externalProxy { var reverses any var stream map[string]any json.Unmarshal(ep.StreamSettings, &stream) if tlsSettings, ok := stream["tlsSettings"].(map[string]any); ok { if settings, ok := tlsSettings["settings"].(map[string]any); ok { if domains, ok := settings["domains"].([]any); ok { for _, domain := range domains { if domainMap, ok := domain.(map[string]any); ok { domainMap["forceTls"] = "same" domainMap["port"] = ep.Port domainMap["dest"] = domainMap["domain"].(string) delete(domainMap, "domain") } } } reverses = settings["domains"] delete(settings, "domains") } } stream["externalProxy"] = reverses newStream, _ := json.MarshalIndent(stream, " ", " ") tx.Model(model.Inbound{}).Where("id = ?", ep.Id).Update("stream_settings", newStream) } err = tx.Raw(`UPDATE inbounds SET tag = REPLACE(tag, '0.0.0.0:', '') WHERE INSTR(tag, '0.0.0.0:') > 0;`).Error if err != nil { return } } func (s *InboundService) MigrateDB() { s.MigrationRequirements() s.MigrationRemoveOrphanedTraffics() } func (s *InboundService) GetOnlineClients() []string { return p.GetOnlineClients() } func (s *InboundService) GetClientsLastOnline() (map[string]int64, error) { db := database.GetDB() var rows []xray.ClientTraffic err := db.Model(&xray.ClientTraffic{}).Select("email, last_online").Find(&rows).Error if err != nil && err != gorm.ErrRecordNotFound { return nil, err } result := make(map[string]int64, len(rows)) for _, r := range rows { result[r.Email] = r.LastOnline } return result, nil } func (s *InboundService) FilterAndSortClientEmails(emails []string) ([]string, []string, error) { db := database.GetDB() // Step 1: Get ClientTraffic records for emails in the input list var clients []xray.ClientTraffic err := db.Where("email IN ?", emails).Find(&clients).Error if err != nil && err != gorm.ErrRecordNotFound { return nil, nil, err } // Step 2: Sort clients by (Up + Down) descending sort.Slice(clients, func(i, j int) bool { return (clients[i].Up + clients[i].Down) > (clients[j].Up + clients[j].Down) }) // Step 3: Extract sorted valid emails and track found ones validEmails := make([]string, 0, len(clients)) found := make(map[string]bool) for _, client := range clients { validEmails = append(validEmails, client.Email) found[client.Email] = true } // Step 4: Identify emails that were not found in the database extraEmails := make([]string, 0) for _, email := range emails { if !found[email] { extraEmails = append(extraEmails, email) } } return validEmails, extraEmails, nil } func (s *InboundService) DelInboundClientByEmail(inboundId int, email string) (bool, error) { oldInbound, err := s.GetInbound(inboundId) if err != nil { logger.Error("Load Old Data Error") return false, err } var settings map[string]any if err := json.Unmarshal([]byte(oldInbound.Settings), &settings); err != nil { return false, err } interfaceClients, ok := settings["clients"].([]any) if !ok { return false, common.NewError("invalid clients format in inbound settings") } var newClients []any needApiDel := false found := false for _, client := range interfaceClients { c, ok := client.(map[string]any) if !ok { continue } if cEmail, ok := c["email"].(string); ok && cEmail == email { // matched client, drop it found = true needApiDel, _ = c["enable"].(bool) } else { newClients = append(newClients, client) } } if !found { return false, common.NewError(fmt.Sprintf("client with email %s not found", email)) } if len(newClients) == 0 { return false, common.NewError("no client remained in Inbound") } settings["clients"] = newClients newSettings, err := json.MarshalIndent(settings, "", " ") if err != nil { return false, err } oldInbound.Settings = string(newSettings) db := database.GetDB() // remove IP bindings if err := s.DelClientIPs(db, email); err != nil { logger.Error("Error in delete client IPs") return false, err } needRestart := false // remove stats too if len(email) > 0 { traffic, err := s.GetClientTrafficByEmail(email) if err != nil { return false, err } if traffic != nil { if err := s.DelClientStat(db, email); err != nil { logger.Error("Delete stats Data Error") return false, err } } if needApiDel { s.xrayApi.Init(p.GetAPIPort()) if err1 := s.xrayApi.RemoveUser(oldInbound.Tag, email); err1 == nil { logger.Debug("Client deleted by api:", email) needRestart = false } else { if strings.Contains(err1.Error(), fmt.Sprintf("User %s not found.", email)) { logger.Debug("User is already deleted. Nothing to do more...") } else { logger.Debug("Error in deleting client by api:", err1) needRestart = true } } s.xrayApi.Close() } } return needRestart, db.Save(oldInbound).Error } ================================================ FILE: web/service/outbound.go ================================================ package service import ( "encoding/json" "fmt" "io" "net" "net/http" "net/url" "os" "sync" "time" "github.com/mhsanaei/3x-ui/v2/config" "github.com/mhsanaei/3x-ui/v2/database" "github.com/mhsanaei/3x-ui/v2/database/model" "github.com/mhsanaei/3x-ui/v2/logger" "github.com/mhsanaei/3x-ui/v2/util/common" "github.com/mhsanaei/3x-ui/v2/util/json_util" "github.com/mhsanaei/3x-ui/v2/xray" "gorm.io/gorm" ) // OutboundService provides business logic for managing Xray outbound configurations. // It handles outbound traffic monitoring and statistics. type OutboundService struct{} // testSemaphore limits concurrent outbound tests to prevent resource exhaustion. var testSemaphore sync.Mutex func (s *OutboundService) AddTraffic(traffics []*xray.Traffic, clientTraffics []*xray.ClientTraffic) (error, bool) { var err error db := database.GetDB() tx := db.Begin() defer func() { if err != nil { tx.Rollback() } else { tx.Commit() } }() err = s.addOutboundTraffic(tx, traffics) if err != nil { return err, false } return nil, false } func (s *OutboundService) addOutboundTraffic(tx *gorm.DB, traffics []*xray.Traffic) error { if len(traffics) == 0 { return nil } var err error for _, traffic := range traffics { if traffic.IsOutbound { var outbound model.OutboundTraffics err = tx.Model(&model.OutboundTraffics{}).Where("tag = ?", traffic.Tag). FirstOrCreate(&outbound).Error if err != nil { return err } outbound.Tag = traffic.Tag outbound.Up = outbound.Up + traffic.Up outbound.Down = outbound.Down + traffic.Down outbound.Total = outbound.Up + outbound.Down err = tx.Save(&outbound).Error if err != nil { return err } } } return nil } func (s *OutboundService) GetOutboundsTraffic() ([]*model.OutboundTraffics, error) { db := database.GetDB() var traffics []*model.OutboundTraffics err := db.Model(model.OutboundTraffics{}).Find(&traffics).Error if err != nil { logger.Warning("Error retrieving OutboundTraffics: ", err) return nil, err } return traffics, nil } func (s *OutboundService) ResetOutboundTraffic(tag string) error { db := database.GetDB() whereText := "tag " if tag == "-alltags-" { whereText += " <> ?" } else { whereText += " = ?" } result := db.Model(model.OutboundTraffics{}). Where(whereText, tag). Updates(map[string]any{"up": 0, "down": 0, "total": 0}) err := result.Error if err != nil { return err } return nil } // TestOutboundResult represents the result of testing an outbound type TestOutboundResult struct { Success bool `json:"success"` Delay int64 `json:"delay"` // Delay in milliseconds Error string `json:"error,omitempty"` StatusCode int `json:"statusCode,omitempty"` } // TestOutbound tests an outbound by creating a temporary xray instance and measuring response time. // allOutboundsJSON must be a JSON array of all outbounds; they are copied into the test config unchanged. // Only the test inbound and a route rule (to the tested outbound tag) are added. func (s *OutboundService) TestOutbound(outboundJSON string, testURL string, allOutboundsJSON string) (*TestOutboundResult, error) { if testURL == "" { testURL = "https://www.google.com/generate_204" } // Limit to one concurrent test at a time if !testSemaphore.TryLock() { return &TestOutboundResult{ Success: false, Error: "Another outbound test is already running, please wait", }, nil } defer testSemaphore.Unlock() // Parse the outbound being tested to get its tag var testOutbound map[string]any if err := json.Unmarshal([]byte(outboundJSON), &testOutbound); err != nil { return &TestOutboundResult{ Success: false, Error: fmt.Sprintf("Invalid outbound JSON: %v", err), }, nil } outboundTag, _ := testOutbound["tag"].(string) if outboundTag == "" { return &TestOutboundResult{ Success: false, Error: "Outbound has no tag", }, nil } if protocol, _ := testOutbound["protocol"].(string); protocol == "blackhole" || outboundTag == "blocked" { return &TestOutboundResult{ Success: false, Error: "Blocked/blackhole outbound cannot be tested", }, nil } // Use all outbounds when provided; otherwise fall back to single outbound var allOutbounds []any if allOutboundsJSON != "" { if err := json.Unmarshal([]byte(allOutboundsJSON), &allOutbounds); err != nil { return &TestOutboundResult{ Success: false, Error: fmt.Sprintf("Invalid allOutbounds JSON: %v", err), }, nil } } if len(allOutbounds) == 0 { allOutbounds = []any{testOutbound} } // Find an available port for test inbound testPort, err := findAvailablePort() if err != nil { return &TestOutboundResult{ Success: false, Error: fmt.Sprintf("Failed to find available port: %v", err), }, nil } // Copy all outbounds as-is, add only test inbound and route rule testConfig := s.createTestConfig(outboundTag, allOutbounds, testPort) // Use a temporary config file so the main config.json is never overwritten testConfigPath, err := createTestConfigPath() if err != nil { return &TestOutboundResult{ Success: false, Error: fmt.Sprintf("Failed to create test config path: %v", err), }, nil } defer os.Remove(testConfigPath) // ensure temp file is removed even if process is not stopped // Create temporary xray process with its own config file testProcess := xray.NewTestProcess(testConfig, testConfigPath) defer func() { if testProcess.IsRunning() { testProcess.Stop() } }() // Start the test process if err := testProcess.Start(); err != nil { return &TestOutboundResult{ Success: false, Error: fmt.Sprintf("Failed to start test xray instance: %v", err), }, nil } // Wait for xray to start listening on the test port if err := waitForPort(testPort, 3*time.Second); err != nil { if !testProcess.IsRunning() { result := testProcess.GetResult() return &TestOutboundResult{ Success: false, Error: fmt.Sprintf("Xray process exited: %s", result), }, nil } return &TestOutboundResult{ Success: false, Error: fmt.Sprintf("Xray failed to start listening: %v", err), }, nil } // Check if process is still running if !testProcess.IsRunning() { result := testProcess.GetResult() return &TestOutboundResult{ Success: false, Error: fmt.Sprintf("Xray process exited: %s", result), }, nil } // Test the connection through proxy delay, statusCode, err := s.testConnection(testPort, testURL) if err != nil { return &TestOutboundResult{ Success: false, Error: err.Error(), }, nil } return &TestOutboundResult{ Success: true, Delay: delay, StatusCode: statusCode, }, nil } // createTestConfig creates a test config by copying all outbounds unchanged and adding // only the test inbound (SOCKS) and a route rule that sends traffic to the given outbound tag. func (s *OutboundService) createTestConfig(outboundTag string, allOutbounds []any, testPort int) *xray.Config { // Test inbound (SOCKS proxy) - only addition to inbounds testInbound := xray.InboundConfig{ Tag: "test-inbound", Listen: json_util.RawMessage(`"127.0.0.1"`), Port: testPort, Protocol: "socks", Settings: json_util.RawMessage(`{"auth":"noauth","udp":true}`), } // Outbounds: copy all, but set noKernelTun=true for WireGuard outbounds processedOutbounds := make([]any, len(allOutbounds)) for i, ob := range allOutbounds { outbound, ok := ob.(map[string]any) if !ok { processedOutbounds[i] = ob continue } if protocol, ok := outbound["protocol"].(string); ok && protocol == "wireguard" { // Set noKernelTun to true for WireGuard outbounds if settings, ok := outbound["settings"].(map[string]any); ok { settings["noKernelTun"] = true } else { // Create settings if it doesn't exist outbound["settings"] = map[string]any{ "noKernelTun": true, } } } processedOutbounds[i] = outbound } outboundsJSON, _ := json.Marshal(processedOutbounds) // Create routing rule to route all traffic through test outbound routingRules := []map[string]any{ { "type": "field", "outboundTag": outboundTag, "network": "tcp,udp", }, } routingJSON, _ := json.Marshal(map[string]any{ "domainStrategy": "AsIs", "rules": routingRules, }) // Disable logging for test process to avoid creating orphaned log files logConfig := map[string]any{ "loglevel": "warning", "access": "none", "error": "none", "dnsLog": false, } logJSON, _ := json.Marshal(logConfig) // Create minimal config cfg := &xray.Config{ LogConfig: json_util.RawMessage(logJSON), InboundConfigs: []xray.InboundConfig{ testInbound, }, OutboundConfigs: json_util.RawMessage(string(outboundsJSON)), RouterConfig: json_util.RawMessage(string(routingJSON)), Policy: json_util.RawMessage(`{}`), Stats: json_util.RawMessage(`{}`), } return cfg } // testConnection tests the connection through the proxy and measures delay. // It performs a warmup request first to establish the SOCKS connection and populate DNS caches, // then measures the second request for a more accurate latency reading. func (s *OutboundService) testConnection(proxyPort int, testURL string) (int64, int, error) { // Create SOCKS5 proxy URL proxyURL := fmt.Sprintf("socks5://127.0.0.1:%d", proxyPort) // Parse proxy URL proxyURLParsed, err := url.Parse(proxyURL) if err != nil { return 0, 0, common.NewErrorf("Invalid proxy URL: %v", err) } // Create HTTP client with proxy and keep-alive for connection reuse client := &http.Client{ Timeout: 10 * time.Second, Transport: &http.Transport{ Proxy: http.ProxyURL(proxyURLParsed), DialContext: (&net.Dialer{ Timeout: 5 * time.Second, KeepAlive: 30 * time.Second, }).DialContext, MaxIdleConns: 1, IdleConnTimeout: 10 * time.Second, DisableCompression: true, }, } // Warmup request: establishes SOCKS/TLS connection, DNS, and TCP to the target. // This mirrors real-world usage where connections are reused. warmupResp, err := client.Get(testURL) if err != nil { return 0, 0, common.NewErrorf("Request failed: %v", err) } io.Copy(io.Discard, warmupResp.Body) warmupResp.Body.Close() // Measure the actual request on the warm connection startTime := time.Now() resp, err := client.Get(testURL) delay := time.Since(startTime).Milliseconds() if err != nil { return 0, 0, common.NewErrorf("Request failed: %v", err) } io.Copy(io.Discard, resp.Body) resp.Body.Close() return delay, resp.StatusCode, nil } // waitForPort polls until the given TCP port is accepting connections or the timeout expires. func waitForPort(port int, timeout time.Duration) error { deadline := time.Now().Add(timeout) for time.Now().Before(deadline) { conn, err := net.DialTimeout("tcp", fmt.Sprintf("127.0.0.1:%d", port), 100*time.Millisecond) if err == nil { conn.Close() return nil } time.Sleep(50 * time.Millisecond) } return fmt.Errorf("port %d not ready after %v", port, timeout) } // findAvailablePort finds an available port for testing func findAvailablePort() (int, error) { listener, err := net.Listen("tcp", ":0") if err != nil { return 0, err } defer listener.Close() addr := listener.Addr().(*net.TCPAddr) return addr.Port, nil } // createTestConfigPath returns a unique path for a temporary xray config file in the bin folder. // The temp file is created and closed so the path is reserved; Start() will overwrite it. func createTestConfigPath() (string, error) { tmpFile, err := os.CreateTemp(config.GetBinFolderPath(), "xray_test_*.json") if err != nil { return "", err } path := tmpFile.Name() if err := tmpFile.Close(); err != nil { os.Remove(path) return "", err } return path, nil } ================================================ FILE: web/service/panel.go ================================================ package service import ( "os" "syscall" "time" "github.com/mhsanaei/3x-ui/v2/logger" ) // PanelService provides business logic for panel management operations. // It handles panel restart, updates, and system-level panel controls. type PanelService struct{} func (s *PanelService) RestartPanel(delay time.Duration) error { p, err := os.FindProcess(syscall.Getpid()) if err != nil { return err } go func() { time.Sleep(delay) err := p.Signal(syscall.SIGHUP) if err != nil { logger.Error("failed to send SIGHUP signal:", err) } }() return nil } ================================================ FILE: web/service/server.go ================================================ package service import ( "archive/zip" "bufio" "bytes" "encoding/json" "fmt" "io" "io/fs" "mime/multipart" "net/http" "os" "os/exec" "path/filepath" "regexp" "runtime" "strconv" "strings" "sync" "time" "github.com/mhsanaei/3x-ui/v2/config" "github.com/mhsanaei/3x-ui/v2/database" "github.com/mhsanaei/3x-ui/v2/logger" "github.com/mhsanaei/3x-ui/v2/util/common" "github.com/mhsanaei/3x-ui/v2/util/sys" "github.com/mhsanaei/3x-ui/v2/xray" "github.com/google/uuid" "github.com/shirou/gopsutil/v4/cpu" "github.com/shirou/gopsutil/v4/disk" "github.com/shirou/gopsutil/v4/host" "github.com/shirou/gopsutil/v4/load" "github.com/shirou/gopsutil/v4/mem" "github.com/shirou/gopsutil/v4/net" ) // ProcessState represents the current state of a system process. type ProcessState string // Process state constants const ( Running ProcessState = "running" // Process is running normally Stop ProcessState = "stop" // Process is stopped Error ProcessState = "error" // Process is in error state ) // Status represents comprehensive system and application status information. // It includes CPU, memory, disk, network statistics, and Xray process status. type Status struct { T time.Time `json:"-"` Cpu float64 `json:"cpu"` CpuCores int `json:"cpuCores"` LogicalPro int `json:"logicalPro"` CpuSpeedMhz float64 `json:"cpuSpeedMhz"` Mem struct { Current uint64 `json:"current"` Total uint64 `json:"total"` } `json:"mem"` Swap struct { Current uint64 `json:"current"` Total uint64 `json:"total"` } `json:"swap"` Disk struct { Current uint64 `json:"current"` Total uint64 `json:"total"` } `json:"disk"` Xray struct { State ProcessState `json:"state"` ErrorMsg string `json:"errorMsg"` Version string `json:"version"` } `json:"xray"` Uptime uint64 `json:"uptime"` Loads []float64 `json:"loads"` TcpCount int `json:"tcpCount"` UdpCount int `json:"udpCount"` NetIO struct { Up uint64 `json:"up"` Down uint64 `json:"down"` } `json:"netIO"` NetTraffic struct { Sent uint64 `json:"sent"` Recv uint64 `json:"recv"` } `json:"netTraffic"` PublicIP struct { IPv4 string `json:"ipv4"` IPv6 string `json:"ipv6"` } `json:"publicIP"` AppStats struct { Threads uint32 `json:"threads"` Mem uint64 `json:"mem"` Uptime uint64 `json:"uptime"` } `json:"appStats"` } // Release represents information about a software release from GitHub. type Release struct { TagName string `json:"tag_name"` // The tag name of the release } // ServerService provides business logic for server monitoring and management. // It handles system status collection, IP detection, and application statistics. type ServerService struct { xrayService XrayService inboundService InboundService cachedIPv4 string cachedIPv6 string noIPv6 bool mu sync.Mutex lastCPUTimes cpu.TimesStat hasLastCPUSample bool hasNativeCPUSample bool emaCPU float64 cpuHistory []CPUSample cachedCpuSpeedMhz float64 lastCpuInfoAttempt time.Time } // AggregateCpuHistory returns up to maxPoints averaged buckets of size bucketSeconds over recent data. func (s *ServerService) AggregateCpuHistory(bucketSeconds int, maxPoints int) []map[string]any { if bucketSeconds <= 0 || maxPoints <= 0 { return nil } cutoff := time.Now().Add(-time.Duration(bucketSeconds*maxPoints) * time.Second).Unix() s.mu.Lock() // find start index (history sorted ascending) hist := s.cpuHistory // binary-ish scan (simple linear from end since size capped ~10800 is fine) startIdx := 0 for i := len(hist) - 1; i >= 0; i-- { if hist[i].T < cutoff { startIdx = i + 1 break } } if startIdx >= len(hist) { s.mu.Unlock() return []map[string]any{} } slice := hist[startIdx:] // copy for unlock tmp := make([]CPUSample, len(slice)) copy(tmp, slice) s.mu.Unlock() if len(tmp) == 0 { return []map[string]any{} } var out []map[string]any var acc []float64 bSize := int64(bucketSeconds) curBucket := (tmp[0].T / bSize) * bSize flush := func(ts int64) { if len(acc) == 0 { return } sum := 0.0 for _, v := range acc { sum += v } avg := sum / float64(len(acc)) out = append(out, map[string]any{"t": ts, "cpu": avg}) acc = acc[:0] } for _, p := range tmp { b := (p.T / bSize) * bSize if b != curBucket { flush(curBucket) curBucket = b } acc = append(acc, p.Cpu) } flush(curBucket) if len(out) > maxPoints { out = out[len(out)-maxPoints:] } return out } // CPUSample single CPU utilization sample type CPUSample struct { T int64 `json:"t"` // unix seconds Cpu float64 `json:"cpu"` // percent 0..100 } type LogEntry struct { DateTime time.Time FromAddress string ToAddress string Inbound string Outbound string Email string Event int } func getPublicIP(url string) string { client := &http.Client{ Timeout: 3 * time.Second, } resp, err := client.Get(url) if err != nil { return "N/A" } defer resp.Body.Close() // Don't retry if access is blocked or region-restricted if resp.StatusCode == http.StatusForbidden || resp.StatusCode == http.StatusUnavailableForLegalReasons { return "N/A" } if resp.StatusCode != http.StatusOK { return "N/A" } ip, err := io.ReadAll(resp.Body) if err != nil { return "N/A" } ipString := strings.TrimSpace(string(ip)) if ipString == "" { return "N/A" } return ipString } func (s *ServerService) GetStatus(lastStatus *Status) *Status { now := time.Now() status := &Status{ T: now, } // CPU stats util, err := s.sampleCPUUtilization() if err != nil { logger.Warning("get cpu percent failed:", err) } else { status.Cpu = util } status.CpuCores, err = cpu.Counts(false) if err != nil { logger.Warning("get cpu cores count failed:", err) } status.LogicalPro = runtime.NumCPU() if status.CpuSpeedMhz = s.cachedCpuSpeedMhz; s.cachedCpuSpeedMhz == 0 && time.Since(s.lastCpuInfoAttempt) > 5*time.Minute { s.lastCpuInfoAttempt = time.Now() done := make(chan struct{}) go func() { defer close(done) cpuInfos, err := cpu.Info() if err != nil { logger.Warning("get cpu info failed:", err) return } if len(cpuInfos) > 0 { s.cachedCpuSpeedMhz = cpuInfos[0].Mhz status.CpuSpeedMhz = s.cachedCpuSpeedMhz } else { logger.Warning("could not find cpu info") } }() select { case <-done: case <-time.After(1500 * time.Millisecond): logger.Warning("cpu info query timed out; will retry later") } } else if s.cachedCpuSpeedMhz != 0 { status.CpuSpeedMhz = s.cachedCpuSpeedMhz } // Uptime upTime, err := host.Uptime() if err != nil { logger.Warning("get uptime failed:", err) } else { status.Uptime = upTime } // Memory stats memInfo, err := mem.VirtualMemory() if err != nil { logger.Warning("get virtual memory failed:", err) } else { status.Mem.Current = memInfo.Used status.Mem.Total = memInfo.Total } swapInfo, err := mem.SwapMemory() if err != nil { logger.Warning("get swap memory failed:", err) } else { status.Swap.Current = swapInfo.Used status.Swap.Total = swapInfo.Total } // Disk stats diskInfo, err := disk.Usage("/") if err != nil { logger.Warning("get disk usage failed:", err) } else { status.Disk.Current = diskInfo.Used status.Disk.Total = diskInfo.Total } // Load averages avgState, err := load.Avg() if err != nil { logger.Warning("get load avg failed:", err) } else { status.Loads = []float64{avgState.Load1, avgState.Load5, avgState.Load15} } // Network stats ioStats, err := net.IOCounters(false) if err != nil { logger.Warning("get io counters failed:", err) } else if len(ioStats) > 0 { ioStat := ioStats[0] status.NetTraffic.Sent = ioStat.BytesSent status.NetTraffic.Recv = ioStat.BytesRecv if lastStatus != nil { duration := now.Sub(lastStatus.T) seconds := float64(duration) / float64(time.Second) up := uint64(float64(status.NetTraffic.Sent-lastStatus.NetTraffic.Sent) / seconds) down := uint64(float64(status.NetTraffic.Recv-lastStatus.NetTraffic.Recv) / seconds) status.NetIO.Up = up status.NetIO.Down = down } } else { logger.Warning("can not find io counters") } // TCP/UDP connections status.TcpCount, err = sys.GetTCPCount() if err != nil { logger.Warning("get tcp connections failed:", err) } status.UdpCount, err = sys.GetUDPCount() if err != nil { logger.Warning("get udp connections failed:", err) } // IP fetching with caching showIp4ServiceLists := []string{ "https://api4.ipify.org", "https://ipv4.icanhazip.com", "https://v4.api.ipinfo.io/ip", "https://ipv4.myexternalip.com/raw", "https://4.ident.me", "https://check-host.net/ip", } showIp6ServiceLists := []string{ "https://api6.ipify.org", "https://ipv6.icanhazip.com", "https://v6.api.ipinfo.io/ip", "https://ipv6.myexternalip.com/raw", "https://6.ident.me", } if s.cachedIPv4 == "" { for _, ip4Service := range showIp4ServiceLists { s.cachedIPv4 = getPublicIP(ip4Service) if s.cachedIPv4 != "N/A" { break } } } if s.cachedIPv6 == "" && !s.noIPv6 { for _, ip6Service := range showIp6ServiceLists { s.cachedIPv6 = getPublicIP(ip6Service) if s.cachedIPv6 != "N/A" { break } } } if s.cachedIPv6 == "N/A" { s.noIPv6 = true } status.PublicIP.IPv4 = s.cachedIPv4 status.PublicIP.IPv6 = s.cachedIPv6 // Xray status if s.xrayService.IsXrayRunning() { status.Xray.State = Running status.Xray.ErrorMsg = "" } else { err := s.xrayService.GetXrayErr() if err != nil { status.Xray.State = Error } else { status.Xray.State = Stop } status.Xray.ErrorMsg = s.xrayService.GetXrayResult() } status.Xray.Version = s.xrayService.GetXrayVersion() // Application stats var rtm runtime.MemStats runtime.ReadMemStats(&rtm) status.AppStats.Mem = rtm.Sys status.AppStats.Threads = uint32(runtime.NumGoroutine()) if p != nil && p.IsRunning() { status.AppStats.Uptime = p.GetUptime() } else { status.AppStats.Uptime = 0 } return status } func (s *ServerService) AppendCpuSample(t time.Time, v float64) { const capacity = 9000 // ~5 hours @ 2s interval s.mu.Lock() defer s.mu.Unlock() p := CPUSample{T: t.Unix(), Cpu: v} if n := len(s.cpuHistory); n > 0 && s.cpuHistory[n-1].T == p.T { s.cpuHistory[n-1] = p } else { s.cpuHistory = append(s.cpuHistory, p) } if len(s.cpuHistory) > capacity { s.cpuHistory = s.cpuHistory[len(s.cpuHistory)-capacity:] } } func (s *ServerService) sampleCPUUtilization() (float64, error) { // Try native platform-specific CPU implementation first (Windows, Linux, macOS) if pct, err := sys.CPUPercentRaw(); err == nil { s.mu.Lock() // First call to native method returns 0 (initializes baseline) if !s.hasNativeCPUSample { s.hasNativeCPUSample = true s.mu.Unlock() return 0, nil } // Smooth with EMA const alpha = 0.3 if s.emaCPU == 0 { s.emaCPU = pct } else { s.emaCPU = alpha*pct + (1-alpha)*s.emaCPU } val := s.emaCPU s.mu.Unlock() return val, nil } // If native call fails, fall back to gopsutil times // Read aggregate CPU times (all CPUs combined) times, err := cpu.Times(false) if err != nil { return 0, err } if len(times) == 0 { return 0, fmt.Errorf("no cpu times available") } cur := times[0] s.mu.Lock() defer s.mu.Unlock() // If this is the first sample, initialize and return current EMA (0 by default) if !s.hasLastCPUSample { s.lastCPUTimes = cur s.hasLastCPUSample = true return s.emaCPU, nil } // Compute busy and total deltas // Note: Guest and GuestNice times are already included in User and Nice respectively, // so we exclude them to avoid double-counting (Linux kernel accounting) idleDelta := cur.Idle - s.lastCPUTimes.Idle busyDelta := (cur.User - s.lastCPUTimes.User) + (cur.System - s.lastCPUTimes.System) + (cur.Nice - s.lastCPUTimes.Nice) + (cur.Iowait - s.lastCPUTimes.Iowait) + (cur.Irq - s.lastCPUTimes.Irq) + (cur.Softirq - s.lastCPUTimes.Softirq) + (cur.Steal - s.lastCPUTimes.Steal) totalDelta := busyDelta + idleDelta // Update last sample for next time s.lastCPUTimes = cur // Guard against division by zero or negative deltas (e.g., counter resets) if totalDelta <= 0 { return s.emaCPU, nil } raw := 100.0 * (busyDelta / totalDelta) if raw < 0 { raw = 0 } if raw > 100 { raw = 100 } // Exponential moving average to smooth spikes const alpha = 0.3 // smoothing factor (0 26 || (major == 26 && minor > 2) || (major == 26 && minor == 2 && patch >= 6) { versions = append(versions, release.TagName) } } return versions, nil } func (s *ServerService) StopXrayService() error { err := s.xrayService.StopXray() if err != nil { logger.Error("stop xray failed:", err) return err } return nil } func (s *ServerService) RestartXrayService() error { err := s.xrayService.RestartXray(true) if err != nil { logger.Error("start xray failed:", err) return err } return nil } func (s *ServerService) downloadXRay(version string) (string, error) { osName := runtime.GOOS arch := runtime.GOARCH switch osName { case "darwin": osName = "macos" case "windows": osName = "windows" } switch arch { case "amd64": arch = "64" case "arm64": arch = "arm64-v8a" case "armv7": arch = "arm32-v7a" case "armv6": arch = "arm32-v6" case "armv5": arch = "arm32-v5" case "386": arch = "32" case "s390x": arch = "s390x" } fileName := fmt.Sprintf("Xray-%s-%s.zip", osName, arch) url := fmt.Sprintf("https://github.com/XTLS/Xray-core/releases/download/%s/%s", version, fileName) resp, err := http.Get(url) if err != nil { return "", err } defer resp.Body.Close() os.Remove(fileName) file, err := os.Create(fileName) if err != nil { return "", err } defer file.Close() _, err = io.Copy(file, resp.Body) if err != nil { return "", err } return fileName, nil } func (s *ServerService) UpdateXray(version string) error { // 1. Stop xray before doing anything if err := s.StopXrayService(); err != nil { logger.Warning("failed to stop xray before update:", err) } // 2. Download the zip zipFileName, err := s.downloadXRay(version) if err != nil { return err } defer os.Remove(zipFileName) zipFile, err := os.Open(zipFileName) if err != nil { return err } defer zipFile.Close() stat, err := zipFile.Stat() if err != nil { return err } reader, err := zip.NewReader(zipFile, stat.Size()) if err != nil { return err } // 3. Helper to extract files copyZipFile := func(zipName string, fileName string) error { zipFile, err := reader.Open(zipName) if err != nil { return err } defer zipFile.Close() os.MkdirAll(filepath.Dir(fileName), 0755) os.Remove(fileName) file, err := os.OpenFile(fileName, os.O_CREATE|os.O_RDWR|os.O_TRUNC, fs.ModePerm) if err != nil { return err } defer file.Close() _, err = io.Copy(file, zipFile) return err } // 4. Extract correct binary if runtime.GOOS == "windows" { targetBinary := filepath.Join("bin", "xray-windows-amd64.exe") err = copyZipFile("xray.exe", targetBinary) } else { err = copyZipFile("xray", xray.GetBinaryPath()) } if err != nil { return err } // 5. Restart xray if err := s.xrayService.RestartXray(true); err != nil { logger.Error("start xray failed:", err) return err } return nil } func (s *ServerService) GetLogs(count string, level string, syslog string) []string { c, _ := strconv.Atoi(count) var lines []string if syslog == "true" { // Check if running on Windows - journalctl is not available if runtime.GOOS == "windows" { return []string{"Syslog is not supported on Windows. Please use application logs instead by unchecking the 'Syslog' option."} } // Validate and sanitize count parameter countInt, err := strconv.Atoi(count) if err != nil || countInt < 1 || countInt > 10000 { return []string{"Invalid count parameter - must be a number between 1 and 10000"} } // Validate level parameter - only allow valid syslog levels validLevels := map[string]bool{ "0": true, "emerg": true, "1": true, "alert": true, "2": true, "crit": true, "3": true, "err": true, "4": true, "warning": true, "5": true, "notice": true, "6": true, "info": true, "7": true, "debug": true, } if !validLevels[level] { return []string{"Invalid level parameter - must be a valid syslog level"} } // Use hardcoded command with validated parameters cmd := exec.Command("journalctl", "-u", "x-ui", "--no-pager", "-n", strconv.Itoa(countInt), "-p", level) var out bytes.Buffer cmd.Stdout = &out err = cmd.Run() if err != nil { return []string{"Failed to run journalctl command! Make sure systemd is available and x-ui service is registered."} } lines = strings.Split(out.String(), "\n") } else { lines = logger.GetLogs(c, level) } return lines } func (s *ServerService) GetXrayLogs( count string, filter string, showDirect string, showBlocked string, showProxy string, freedoms []string, blackholes []string) []LogEntry { const ( Direct = iota Blocked Proxied ) countInt, _ := strconv.Atoi(count) var entries []LogEntry pathToAccessLog, err := xray.GetAccessLogPath() if err != nil { return nil } file, err := os.Open(pathToAccessLog) if err != nil { return nil } defer file.Close() scanner := bufio.NewScanner(file) for scanner.Scan() { line := strings.TrimSpace(scanner.Text()) if line == "" || strings.Contains(line, "api -> api") { //skipping empty lines and api calls continue } if filter != "" && !strings.Contains(line, filter) { //applying filter if it's not empty continue } var entry LogEntry parts := strings.Fields(line) for i, part := range parts { if i == 0 { dateTime, err := time.ParseInLocation("2006/01/02 15:04:05.999999", parts[0]+" "+parts[1], time.Local) if err != nil { continue } entry.DateTime = dateTime.UTC() } if part == "from" { entry.FromAddress = strings.TrimLeft(parts[i+1], "/") } else if part == "accepted" { entry.ToAddress = strings.TrimLeft(parts[i+1], "/") } else if strings.HasPrefix(part, "[") { entry.Inbound = part[1:] } else if strings.HasSuffix(part, "]") { entry.Outbound = part[:len(part)-1] } else if part == "email:" { entry.Email = parts[i+1] } } if logEntryContains(line, freedoms) { if showDirect == "false" { continue } entry.Event = Direct } else if logEntryContains(line, blackholes) { if showBlocked == "false" { continue } entry.Event = Blocked } else { if showProxy == "false" { continue } entry.Event = Proxied } entries = append(entries, entry) } if len(entries) > countInt { entries = entries[len(entries)-countInt:] } return entries } func logEntryContains(line string, suffixes []string) bool { for _, sfx := range suffixes { if strings.Contains(line, sfx+"]") { return true } } return false } func (s *ServerService) GetConfigJson() (any, error) { config, err := s.xrayService.GetXrayConfig() if err != nil { return nil, err } contents, err := json.MarshalIndent(config, "", " ") if err != nil { return nil, err } var jsonData any err = json.Unmarshal(contents, &jsonData) if err != nil { return nil, err } return jsonData, nil } func (s *ServerService) GetDb() ([]byte, error) { // Update by manually trigger a checkpoint operation err := database.Checkpoint() if err != nil { return nil, err } // Open the file for reading file, err := os.Open(config.GetDBPath()) if err != nil { return nil, err } defer file.Close() // Read the file contents fileContents, err := io.ReadAll(file) if err != nil { return nil, err } return fileContents, nil } func (s *ServerService) ImportDB(file multipart.File) error { // Check if the file is a SQLite database isValidDb, err := database.IsSQLiteDB(file) if err != nil { return common.NewErrorf("Error checking db file format: %v", err) } if !isValidDb { return common.NewError("Invalid db file format") } // Reset the file reader to the beginning _, err = file.Seek(0, 0) if err != nil { return common.NewErrorf("Error resetting file reader: %v", err) } // Save the file as a temporary file tempPath := fmt.Sprintf("%s.temp", config.GetDBPath()) // Remove the existing temporary file (if any) if _, err := os.Stat(tempPath); err == nil { if errRemove := os.Remove(tempPath); errRemove != nil { return common.NewErrorf("Error removing existing temporary db file: %v", errRemove) } } // Create the temporary file tempFile, err := os.Create(tempPath) if err != nil { return common.NewErrorf("Error creating temporary db file: %v", err) } // Robust deferred cleanup for the temporary file defer func() { if tempFile != nil { if cerr := tempFile.Close(); cerr != nil { logger.Warningf("Warning: failed to close temp file: %v", cerr) } } if _, err := os.Stat(tempPath); err == nil { if rerr := os.Remove(tempPath); rerr != nil { logger.Warningf("Warning: failed to remove temp file: %v", rerr) } } }() // Save uploaded file to temporary file if _, err = io.Copy(tempFile, file); err != nil { return common.NewErrorf("Error saving db: %v", err) } // Close temp file before opening via sqlite if err = tempFile.Close(); err != nil { return common.NewErrorf("Error closing temporary db file: %v", err) } tempFile = nil // Validate integrity (no migrations / side effects) if err = database.ValidateSQLiteDB(tempPath); err != nil { return common.NewErrorf("Invalid or corrupt db file: %v", err) } // Stop Xray (ignore error but log) if errStop := s.StopXrayService(); errStop != nil { logger.Warningf("Failed to stop Xray before DB import: %v", errStop) } // Close existing DB to release file locks (especially on Windows) if errClose := database.CloseDB(); errClose != nil { logger.Warningf("Failed to close existing DB before replacement: %v", errClose) } // Backup the current database for fallback fallbackPath := fmt.Sprintf("%s.backup", config.GetDBPath()) // Remove the existing fallback file (if any) if _, err := os.Stat(fallbackPath); err == nil { if errRemove := os.Remove(fallbackPath); errRemove != nil { return common.NewErrorf("Error removing existing fallback db file: %v", errRemove) } } // Move the current database to the fallback location if err = os.Rename(config.GetDBPath(), fallbackPath); err != nil { return common.NewErrorf("Error backing up current db file: %v", err) } // Defer fallback cleanup ONLY if everything goes well defer func() { if _, err := os.Stat(fallbackPath); err == nil { if rerr := os.Remove(fallbackPath); rerr != nil { logger.Warningf("Warning: failed to remove fallback file: %v", rerr) } } }() // Move temp to DB path if err = os.Rename(tempPath, config.GetDBPath()); err != nil { // Restore from fallback if errRename := os.Rename(fallbackPath, config.GetDBPath()); errRename != nil { return common.NewErrorf("Error moving db file and restoring fallback: %v", errRename) } return common.NewErrorf("Error moving db file: %v", err) } // Open & migrate new DB if err = database.InitDB(config.GetDBPath()); err != nil { if errRename := os.Rename(fallbackPath, config.GetDBPath()); errRename != nil { return common.NewErrorf("Error migrating db and restoring fallback: %v", errRename) } return common.NewErrorf("Error migrating db: %v", err) } s.inboundService.MigrateDB() // Start Xray if err = s.RestartXrayService(); err != nil { return common.NewErrorf("Imported DB but failed to start Xray: %v", err) } return nil } // IsValidGeofileName validates that the filename is safe for geofile operations. // It checks for path traversal attempts and ensures the filename contains only safe characters. func (s *ServerService) IsValidGeofileName(filename string) bool { if filename == "" { return false } // Check for path traversal attempts if strings.Contains(filename, "..") { return false } // Check for path separators (both forward and backward slash) if strings.ContainsAny(filename, `/\`) { return false } // Check for absolute path indicators if filepath.IsAbs(filename) { return false } // Additional security: only allow alphanumeric, dots, underscores, and hyphens // This is stricter than the general filename regex validGeofilePattern := `^[a-zA-Z0-9._-]+\.dat$` matched, _ := regexp.MatchString(validGeofilePattern, filename) return matched } func (s *ServerService) UpdateGeofile(fileName string) error { type geofileEntry struct { URL string FileName string } geofileAllowlist := map[string]geofileEntry{ "geoip.dat": {"https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geoip.dat", "geoip.dat"}, "geosite.dat": {"https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geosite.dat", "geosite.dat"}, "geoip_IR.dat": {"https://github.com/chocolate4u/Iran-v2ray-rules/releases/latest/download/geoip.dat", "geoip_IR.dat"}, "geosite_IR.dat": {"https://github.com/chocolate4u/Iran-v2ray-rules/releases/latest/download/geosite.dat", "geosite_IR.dat"}, "geoip_RU.dat": {"https://github.com/runetfreedom/russia-v2ray-rules-dat/releases/latest/download/geoip.dat", "geoip_RU.dat"}, "geosite_RU.dat": {"https://github.com/runetfreedom/russia-v2ray-rules-dat/releases/latest/download/geosite.dat", "geosite_RU.dat"}, } // Strict allowlist check to avoid writing uncontrolled files if fileName != "" { if _, ok := geofileAllowlist[fileName]; !ok { return common.NewErrorf("Invalid geofile name: %q not in allowlist", fileName) } } downloadFile := func(url, destPath string) error { var req *http.Request req, err := http.NewRequest("GET", url, nil) if err != nil { return common.NewErrorf("Failed to create HTTP request for %s: %v", url, err) } var localFileModTime time.Time if fileInfo, err := os.Stat(destPath); err == nil { localFileModTime = fileInfo.ModTime() if !localFileModTime.IsZero() { req.Header.Set("If-Modified-Since", localFileModTime.UTC().Format(http.TimeFormat)) } } client := &http.Client{} resp, err := client.Do(req) if err != nil { return common.NewErrorf("Failed to download Geofile from %s: %v", url, err) } defer resp.Body.Close() // Parse Last-Modified header from server var serverModTime time.Time serverModTimeStr := resp.Header.Get("Last-Modified") if serverModTimeStr != "" { parsedTime, err := time.Parse(http.TimeFormat, serverModTimeStr) if err != nil { logger.Warningf("Failed to parse Last-Modified header for %s: %v", url, err) } else { serverModTime = parsedTime } } // Function to update local file's modification time updateFileModTime := func() { if !serverModTime.IsZero() { if err := os.Chtimes(destPath, serverModTime, serverModTime); err != nil { logger.Warningf("Failed to update modification time for %s: %v", destPath, err) } } } // Handle 304 Not Modified if resp.StatusCode == http.StatusNotModified { updateFileModTime() return nil } if resp.StatusCode != http.StatusOK { return common.NewErrorf("Failed to download Geofile from %s: received status code %d", url, resp.StatusCode) } file, err := os.Create(destPath) if err != nil { return common.NewErrorf("Failed to create Geofile %s: %v", destPath, err) } defer file.Close() _, err = io.Copy(file, resp.Body) if err != nil { return common.NewErrorf("Failed to save Geofile %s: %v", destPath, err) } updateFileModTime() return nil } var errorMessages []string if fileName == "" { // Download all geofiles for _, entry := range geofileAllowlist { destPath := filepath.Join(config.GetBinFolderPath(), entry.FileName) if err := downloadFile(entry.URL, destPath); err != nil { errorMessages = append(errorMessages, fmt.Sprintf("Error downloading Geofile '%s': %v", entry.FileName, err)) } } } else { entry := geofileAllowlist[fileName] destPath := filepath.Join(config.GetBinFolderPath(), entry.FileName) if err := downloadFile(entry.URL, destPath); err != nil { errorMessages = append(errorMessages, fmt.Sprintf("Error downloading Geofile '%s': %v", entry.FileName, err)) } } err := s.RestartXrayService() if err != nil { errorMessages = append(errorMessages, fmt.Sprintf("Updated Geofile '%s' but Failed to start Xray: %v", fileName, err)) } if len(errorMessages) > 0 { return common.NewErrorf("%s", strings.Join(errorMessages, "\r\n")) } return nil } func (s *ServerService) GetNewX25519Cert() (any, error) { // Run the command cmd := exec.Command(xray.GetBinaryPath(), "x25519") var out bytes.Buffer cmd.Stdout = &out err := cmd.Run() if err != nil { return nil, err } lines := strings.Split(out.String(), "\n") privateKeyLine := strings.Split(lines[0], ":") publicKeyLine := strings.Split(lines[1], ":") privateKey := strings.TrimSpace(privateKeyLine[1]) publicKey := strings.TrimSpace(publicKeyLine[1]) keyPair := map[string]any{ "privateKey": privateKey, "publicKey": publicKey, } return keyPair, nil } func (s *ServerService) GetNewmldsa65() (any, error) { // Run the command cmd := exec.Command(xray.GetBinaryPath(), "mldsa65") var out bytes.Buffer cmd.Stdout = &out err := cmd.Run() if err != nil { return nil, err } lines := strings.Split(out.String(), "\n") SeedLine := strings.Split(lines[0], ":") VerifyLine := strings.Split(lines[1], ":") seed := strings.TrimSpace(SeedLine[1]) verify := strings.TrimSpace(VerifyLine[1]) keyPair := map[string]any{ "seed": seed, "verify": verify, } return keyPair, nil } func (s *ServerService) GetNewEchCert(sni string) (any, error) { // Run the command cmd := exec.Command(xray.GetBinaryPath(), "tls", "ech", "--serverName", sni) var out bytes.Buffer cmd.Stdout = &out err := cmd.Run() if err != nil { return nil, err } lines := strings.Split(out.String(), "\n") if len(lines) < 4 { return nil, common.NewError("invalid ech cert") } configList := lines[1] serverKeys := lines[3] return map[string]any{ "echServerKeys": serverKeys, "echConfigList": configList, }, nil } func (s *ServerService) GetNewVlessEnc() (any, error) { cmd := exec.Command(xray.GetBinaryPath(), "vlessenc") var out bytes.Buffer cmd.Stdout = &out if err := cmd.Run(); err != nil { return nil, err } lines := strings.Split(out.String(), "\n") var auths []map[string]string var current map[string]string for _, line := range lines { line = strings.TrimSpace(line) if strings.HasPrefix(line, "Authentication:") { if current != nil { auths = append(auths, current) } current = map[string]string{ "label": strings.TrimSpace(strings.TrimPrefix(line, "Authentication:")), } } else if strings.HasPrefix(line, `"decryption"`) || strings.HasPrefix(line, `"encryption"`) { parts := strings.SplitN(line, ":", 2) if len(parts) == 2 && current != nil { key := strings.Trim(parts[0], `" `) val := strings.Trim(parts[1], `" `) current[key] = val } } } if current != nil { auths = append(auths, current) } return map[string]any{ "auths": auths, }, nil } func (s *ServerService) GetNewUUID() (map[string]string, error) { newUUID, err := uuid.NewRandom() if err != nil { return nil, fmt.Errorf("failed to generate UUID: %w", err) } return map[string]string{ "uuid": newUUID.String(), }, nil } func (s *ServerService) GetNewmlkem768() (any, error) { // Run the command cmd := exec.Command(xray.GetBinaryPath(), "mlkem768") var out bytes.Buffer cmd.Stdout = &out err := cmd.Run() if err != nil { return nil, err } lines := strings.Split(out.String(), "\n") SeedLine := strings.Split(lines[0], ":") ClientLine := strings.Split(lines[1], ":") seed := strings.TrimSpace(SeedLine[1]) client := strings.TrimSpace(ClientLine[1]) keyPair := map[string]any{ "seed": seed, "client": client, } return keyPair, nil } ================================================ FILE: web/service/setting.go ================================================ package service import ( _ "embed" "encoding/json" "errors" "fmt" "net" "reflect" "strconv" "strings" "time" "github.com/mhsanaei/3x-ui/v2/database" "github.com/mhsanaei/3x-ui/v2/database/model" "github.com/mhsanaei/3x-ui/v2/logger" "github.com/mhsanaei/3x-ui/v2/util/common" "github.com/mhsanaei/3x-ui/v2/util/random" "github.com/mhsanaei/3x-ui/v2/util/reflect_util" "github.com/mhsanaei/3x-ui/v2/web/entity" "github.com/mhsanaei/3x-ui/v2/xray" ) //go:embed config.json var xrayTemplateConfig string var defaultValueMap = map[string]string{ "xrayTemplateConfig": xrayTemplateConfig, "webListen": "", "webDomain": "", "webPort": "2053", "webCertFile": "", "webKeyFile": "", "secret": random.Seq(32), "webBasePath": "/", "sessionMaxAge": "360", "pageSize": "25", "expireDiff": "0", "trafficDiff": "0", "remarkModel": "-ieo", "timeLocation": "Local", "tgBotEnable": "false", "tgBotToken": "", "tgBotProxy": "", "tgBotAPIServer": "", "tgBotChatId": "", "tgRunTime": "@daily", "tgBotBackup": "false", "tgBotLoginNotify": "true", "tgCpu": "80", "tgLang": "en-US", "twoFactorEnable": "false", "twoFactorToken": "", "subEnable": "true", "subJsonEnable": "false", "subTitle": "", "subSupportUrl": "", "subProfileUrl": "", "subAnnounce": "", "subEnableRouting": "true", "subRoutingRules": "", "subListen": "", "subPort": "2096", "subPath": "/sub/", "subDomain": "", "subCertFile": "", "subKeyFile": "", "subUpdates": "12", "subEncrypt": "true", "subShowInfo": "true", "subURI": "", "subJsonPath": "/json/", "subJsonURI": "", "subJsonFragment": "", "subJsonNoises": "", "subJsonMux": "", "subJsonRules": "", "datepicker": "gregorian", "warp": "", "externalTrafficInformEnable": "false", "externalTrafficInformURI": "", "xrayOutboundTestUrl": "https://www.google.com/generate_204", // LDAP defaults "ldapEnable": "false", "ldapHost": "", "ldapPort": "389", "ldapUseTLS": "false", "ldapBindDN": "", "ldapPassword": "", "ldapBaseDN": "", "ldapUserFilter": "(objectClass=person)", "ldapUserAttr": "mail", "ldapVlessField": "vless_enabled", "ldapSyncCron": "@every 1m", "ldapFlagField": "", "ldapTruthyValues": "true,1,yes,on", "ldapInvertFlag": "false", "ldapInboundTags": "", "ldapAutoCreate": "false", "ldapAutoDelete": "false", "ldapDefaultTotalGB": "0", "ldapDefaultExpiryDays": "0", "ldapDefaultLimitIP": "0", } // SettingService provides business logic for application settings management. // It handles configuration storage, retrieval, and validation for all system settings. type SettingService struct{} func (s *SettingService) GetDefaultJSONConfig() (any, error) { var jsonData any err := json.Unmarshal([]byte(xrayTemplateConfig), &jsonData) if err != nil { return nil, err } return jsonData, nil } func (s *SettingService) GetAllSetting() (*entity.AllSetting, error) { db := database.GetDB() settings := make([]*model.Setting, 0) err := db.Model(model.Setting{}).Not("key = ?", "xrayTemplateConfig").Find(&settings).Error if err != nil { return nil, err } allSetting := &entity.AllSetting{} t := reflect.TypeFor[entity.AllSetting]() v := reflect.ValueOf(allSetting).Elem() fields := reflect_util.GetFields(t) setSetting := func(key, value string) (err error) { defer func() { panicErr := recover() if panicErr != nil { err = errors.New(fmt.Sprint(panicErr)) } }() var found bool var field reflect.StructField for _, f := range fields { if f.Tag.Get("json") == key { field = f found = true break } } if !found { // Some settings are automatically generated, no need to return to the front end to modify the user return nil } fieldV := v.FieldByName(field.Name) switch t := fieldV.Interface().(type) { case int: n, err := strconv.ParseInt(value, 10, 64) if err != nil { return err } fieldV.SetInt(n) case string: fieldV.SetString(value) case bool: fieldV.SetBool(value == "true") default: return common.NewErrorf("unknown field %v type %v", key, t) } return } keyMap := map[string]bool{} for _, setting := range settings { err := setSetting(setting.Key, setting.Value) if err != nil { return nil, err } keyMap[setting.Key] = true } for key, value := range defaultValueMap { if keyMap[key] { continue } err := setSetting(key, value) if err != nil { return nil, err } } return allSetting, nil } func (s *SettingService) ResetSettings() error { db := database.GetDB() err := db.Where("1 = 1").Delete(model.Setting{}).Error if err != nil { return err } return db.Model(model.User{}). Where("1 = 1").Error } func (s *SettingService) getSetting(key string) (*model.Setting, error) { db := database.GetDB() setting := &model.Setting{} err := db.Model(model.Setting{}).Where("key = ?", key).First(setting).Error if err != nil { return nil, err } return setting, nil } func (s *SettingService) saveSetting(key string, value string) error { setting, err := s.getSetting(key) db := database.GetDB() if database.IsNotFound(err) { return db.Create(&model.Setting{ Key: key, Value: value, }).Error } else if err != nil { return err } setting.Key = key setting.Value = value return db.Save(setting).Error } func (s *SettingService) getString(key string) (string, error) { setting, err := s.getSetting(key) if database.IsNotFound(err) { value, ok := defaultValueMap[key] if !ok { return "", common.NewErrorf("key <%v> not in defaultValueMap", key) } return value, nil } else if err != nil { return "", err } return setting.Value, nil } func (s *SettingService) setString(key string, value string) error { return s.saveSetting(key, value) } func (s *SettingService) getBool(key string) (bool, error) { str, err := s.getString(key) if err != nil { return false, err } return strconv.ParseBool(str) } func (s *SettingService) setBool(key string, value bool) error { return s.setString(key, strconv.FormatBool(value)) } func (s *SettingService) getInt(key string) (int, error) { str, err := s.getString(key) if err != nil { return 0, err } return strconv.Atoi(str) } func (s *SettingService) setInt(key string, value int) error { return s.setString(key, strconv.Itoa(value)) } func (s *SettingService) GetXrayConfigTemplate() (string, error) { return s.getString("xrayTemplateConfig") } func (s *SettingService) GetXrayOutboundTestUrl() (string, error) { return s.getString("xrayOutboundTestUrl") } func (s *SettingService) SetXrayOutboundTestUrl(url string) error { return s.setString("xrayOutboundTestUrl", url) } func (s *SettingService) GetListen() (string, error) { return s.getString("webListen") } func (s *SettingService) SetListen(ip string) error { return s.setString("webListen", ip) } func (s *SettingService) GetWebDomain() (string, error) { return s.getString("webDomain") } func (s *SettingService) GetTgBotToken() (string, error) { return s.getString("tgBotToken") } func (s *SettingService) SetTgBotToken(token string) error { return s.setString("tgBotToken", token) } func (s *SettingService) GetTgBotProxy() (string, error) { return s.getString("tgBotProxy") } func (s *SettingService) SetTgBotProxy(token string) error { return s.setString("tgBotProxy", token) } func (s *SettingService) GetTgBotAPIServer() (string, error) { return s.getString("tgBotAPIServer") } func (s *SettingService) SetTgBotAPIServer(token string) error { return s.setString("tgBotAPIServer", token) } func (s *SettingService) GetTgBotChatId() (string, error) { return s.getString("tgBotChatId") } func (s *SettingService) SetTgBotChatId(chatIds string) error { return s.setString("tgBotChatId", chatIds) } func (s *SettingService) GetTgbotEnabled() (bool, error) { return s.getBool("tgBotEnable") } func (s *SettingService) SetTgbotEnabled(value bool) error { return s.setBool("tgBotEnable", value) } func (s *SettingService) GetTgbotRuntime() (string, error) { return s.getString("tgRunTime") } func (s *SettingService) SetTgbotRuntime(time string) error { return s.setString("tgRunTime", time) } func (s *SettingService) GetTgBotBackup() (bool, error) { return s.getBool("tgBotBackup") } func (s *SettingService) GetTgBotLoginNotify() (bool, error) { return s.getBool("tgBotLoginNotify") } func (s *SettingService) GetTgCpu() (int, error) { return s.getInt("tgCpu") } func (s *SettingService) GetTgLang() (string, error) { return s.getString("tgLang") } func (s *SettingService) GetTwoFactorEnable() (bool, error) { return s.getBool("twoFactorEnable") } func (s *SettingService) SetTwoFactorEnable(value bool) error { return s.setBool("twoFactorEnable", value) } func (s *SettingService) GetTwoFactorToken() (string, error) { return s.getString("twoFactorToken") } func (s *SettingService) SetTwoFactorToken(value string) error { return s.setString("twoFactorToken", value) } func (s *SettingService) GetPort() (int, error) { return s.getInt("webPort") } func (s *SettingService) SetPort(port int) error { return s.setInt("webPort", port) } func (s *SettingService) SetCertFile(webCertFile string) error { return s.setString("webCertFile", webCertFile) } func (s *SettingService) GetCertFile() (string, error) { return s.getString("webCertFile") } func (s *SettingService) SetKeyFile(webKeyFile string) error { return s.setString("webKeyFile", webKeyFile) } func (s *SettingService) GetKeyFile() (string, error) { return s.getString("webKeyFile") } func (s *SettingService) GetExpireDiff() (int, error) { return s.getInt("expireDiff") } func (s *SettingService) GetTrafficDiff() (int, error) { return s.getInt("trafficDiff") } func (s *SettingService) GetSessionMaxAge() (int, error) { return s.getInt("sessionMaxAge") } func (s *SettingService) GetRemarkModel() (string, error) { return s.getString("remarkModel") } func (s *SettingService) GetSecret() ([]byte, error) { secret, err := s.getString("secret") if secret == defaultValueMap["secret"] { err := s.saveSetting("secret", secret) if err != nil { logger.Warning("save secret failed:", err) } } return []byte(secret), err } func (s *SettingService) SetBasePath(basePath string) error { if !strings.HasPrefix(basePath, "/") { basePath = "/" + basePath } if !strings.HasSuffix(basePath, "/") { basePath += "/" } return s.setString("webBasePath", basePath) } func (s *SettingService) GetBasePath() (string, error) { basePath, err := s.getString("webBasePath") if err != nil { return "", err } if !strings.HasPrefix(basePath, "/") { basePath = "/" + basePath } if !strings.HasSuffix(basePath, "/") { basePath += "/" } return basePath, nil } func (s *SettingService) GetTimeLocation() (*time.Location, error) { l, err := s.getString("timeLocation") if err != nil { return nil, err } location, err := time.LoadLocation(l) if err != nil { defaultLocation := defaultValueMap["timeLocation"] logger.Errorf("location <%v> not exist, using default location: %v", l, defaultLocation) return time.LoadLocation(defaultLocation) } return location, nil } func (s *SettingService) GetSubEnable() (bool, error) { return s.getBool("subEnable") } func (s *SettingService) GetSubJsonEnable() (bool, error) { return s.getBool("subJsonEnable") } func (s *SettingService) GetSubTitle() (string, error) { return s.getString("subTitle") } func (s *SettingService) GetSubSupportUrl() (string, error) { return s.getString("subSupportUrl") } func (s *SettingService) GetSubProfileUrl() (string, error) { return s.getString("subProfileUrl") } func (s *SettingService) GetSubAnnounce() (string, error) { return s.getString("subAnnounce") } func (s *SettingService) GetSubEnableRouting() (bool, error) { return s.getBool("subEnableRouting") } func (s *SettingService) GetSubRoutingRules() (string, error) { return s.getString("subRoutingRules") } func (s *SettingService) GetSubListen() (string, error) { return s.getString("subListen") } func (s *SettingService) GetSubPort() (int, error) { return s.getInt("subPort") } func (s *SettingService) GetSubPath() (string, error) { return s.getString("subPath") } func (s *SettingService) GetSubJsonPath() (string, error) { return s.getString("subJsonPath") } func (s *SettingService) GetSubDomain() (string, error) { return s.getString("subDomain") } func (s *SettingService) SetSubCertFile(subCertFile string) error { return s.setString("subCertFile", subCertFile) } func (s *SettingService) GetSubCertFile() (string, error) { return s.getString("subCertFile") } func (s *SettingService) SetSubKeyFile(subKeyFile string) error { return s.setString("subKeyFile", subKeyFile) } func (s *SettingService) GetSubKeyFile() (string, error) { return s.getString("subKeyFile") } func (s *SettingService) GetSubUpdates() (string, error) { return s.getString("subUpdates") } func (s *SettingService) GetSubEncrypt() (bool, error) { return s.getBool("subEncrypt") } func (s *SettingService) GetSubShowInfo() (bool, error) { return s.getBool("subShowInfo") } func (s *SettingService) GetPageSize() (int, error) { return s.getInt("pageSize") } func (s *SettingService) GetSubURI() (string, error) { return s.getString("subURI") } func (s *SettingService) GetSubJsonURI() (string, error) { return s.getString("subJsonURI") } func (s *SettingService) GetSubJsonFragment() (string, error) { return s.getString("subJsonFragment") } func (s *SettingService) GetSubJsonNoises() (string, error) { return s.getString("subJsonNoises") } func (s *SettingService) GetSubJsonMux() (string, error) { return s.getString("subJsonMux") } func (s *SettingService) GetSubJsonRules() (string, error) { return s.getString("subJsonRules") } func (s *SettingService) GetDatepicker() (string, error) { return s.getString("datepicker") } func (s *SettingService) GetWarp() (string, error) { return s.getString("warp") } func (s *SettingService) SetWarp(data string) error { return s.setString("warp", data) } func (s *SettingService) GetExternalTrafficInformEnable() (bool, error) { return s.getBool("externalTrafficInformEnable") } func (s *SettingService) SetExternalTrafficInformEnable(value bool) error { return s.setBool("externalTrafficInformEnable", value) } func (s *SettingService) GetExternalTrafficInformURI() (string, error) { return s.getString("externalTrafficInformURI") } func (s *SettingService) SetExternalTrafficInformURI(InformURI string) error { return s.setString("externalTrafficInformURI", InformURI) } func (s *SettingService) GetIpLimitEnable() (bool, error) { accessLogPath, err := xray.GetAccessLogPath() if err != nil { return false, err } return (accessLogPath != "none" && accessLogPath != ""), nil } // GetLdapEnable returns whether LDAP is enabled. func (s *SettingService) GetLdapEnable() (bool, error) { return s.getBool("ldapEnable") } func (s *SettingService) GetLdapHost() (string, error) { return s.getString("ldapHost") } func (s *SettingService) GetLdapPort() (int, error) { return s.getInt("ldapPort") } func (s *SettingService) GetLdapUseTLS() (bool, error) { return s.getBool("ldapUseTLS") } func (s *SettingService) GetLdapBindDN() (string, error) { return s.getString("ldapBindDN") } func (s *SettingService) GetLdapPassword() (string, error) { return s.getString("ldapPassword") } func (s *SettingService) GetLdapBaseDN() (string, error) { return s.getString("ldapBaseDN") } func (s *SettingService) GetLdapUserFilter() (string, error) { return s.getString("ldapUserFilter") } func (s *SettingService) GetLdapUserAttr() (string, error) { return s.getString("ldapUserAttr") } func (s *SettingService) GetLdapVlessField() (string, error) { return s.getString("ldapVlessField") } func (s *SettingService) GetLdapSyncCron() (string, error) { return s.getString("ldapSyncCron") } func (s *SettingService) GetLdapFlagField() (string, error) { return s.getString("ldapFlagField") } func (s *SettingService) GetLdapTruthyValues() (string, error) { return s.getString("ldapTruthyValues") } func (s *SettingService) GetLdapInvertFlag() (bool, error) { return s.getBool("ldapInvertFlag") } func (s *SettingService) GetLdapInboundTags() (string, error) { return s.getString("ldapInboundTags") } func (s *SettingService) GetLdapAutoCreate() (bool, error) { return s.getBool("ldapAutoCreate") } func (s *SettingService) GetLdapAutoDelete() (bool, error) { return s.getBool("ldapAutoDelete") } func (s *SettingService) GetLdapDefaultTotalGB() (int, error) { return s.getInt("ldapDefaultTotalGB") } func (s *SettingService) GetLdapDefaultExpiryDays() (int, error) { return s.getInt("ldapDefaultExpiryDays") } func (s *SettingService) GetLdapDefaultLimitIP() (int, error) { return s.getInt("ldapDefaultLimitIP") } func (s *SettingService) UpdateAllSetting(allSetting *entity.AllSetting) error { if err := allSetting.CheckValid(); err != nil { return err } v := reflect.ValueOf(allSetting).Elem() t := reflect.TypeFor[entity.AllSetting]() fields := reflect_util.GetFields(t) errs := make([]error, 0) for _, field := range fields { key := field.Tag.Get("json") fieldV := v.FieldByName(field.Name) value := fmt.Sprint(fieldV.Interface()) err := s.saveSetting(key, value) if err != nil { errs = append(errs, err) } } return common.Combine(errs...) } func (s *SettingService) GetDefaultXrayConfig() (any, error) { var jsonData any err := json.Unmarshal([]byte(xrayTemplateConfig), &jsonData) if err != nil { return nil, err } return jsonData, nil } func extractHostname(host string) string { h, _, err := net.SplitHostPort(host) // Err is not nil means host does not contain port if err != nil { h = host } ip := net.ParseIP(h) // If it's not an IP, return as is if ip == nil { return h } // If it's an IPv4, return as is if ip.To4() != nil { return h } // IPv6 needs bracketing return "[" + h + "]" } func (s *SettingService) GetDefaultSettings(host string) (any, error) { type settingFunc func() (any, error) settings := map[string]settingFunc{ "expireDiff": func() (any, error) { return s.GetExpireDiff() }, "trafficDiff": func() (any, error) { return s.GetTrafficDiff() }, "pageSize": func() (any, error) { return s.GetPageSize() }, "defaultCert": func() (any, error) { return s.GetCertFile() }, "defaultKey": func() (any, error) { return s.GetKeyFile() }, "tgBotEnable": func() (any, error) { return s.GetTgbotEnabled() }, "subEnable": func() (any, error) { return s.GetSubEnable() }, "subJsonEnable": func() (any, error) { return s.GetSubJsonEnable() }, "subTitle": func() (any, error) { return s.GetSubTitle() }, "subURI": func() (any, error) { return s.GetSubURI() }, "subJsonURI": func() (any, error) { return s.GetSubJsonURI() }, "remarkModel": func() (any, error) { return s.GetRemarkModel() }, "datepicker": func() (any, error) { return s.GetDatepicker() }, "ipLimitEnable": func() (any, error) { return s.GetIpLimitEnable() }, } result := make(map[string]any) for key, fn := range settings { value, err := fn() if err != nil { return "", err } result[key] = value } subEnable := result["subEnable"].(bool) subJsonEnable := false if v, ok := result["subJsonEnable"]; ok { if b, ok2 := v.(bool); ok2 { subJsonEnable = b } } if (subEnable && result["subURI"].(string) == "") || (subJsonEnable && result["subJsonURI"].(string) == "") { subURI := "" subTitle, _ := s.GetSubTitle() subPort, _ := s.GetSubPort() subPath, _ := s.GetSubPath() subJsonPath, _ := s.GetSubJsonPath() subDomain, _ := s.GetSubDomain() subKeyFile, _ := s.GetSubKeyFile() subCertFile, _ := s.GetSubCertFile() subTLS := false if subKeyFile != "" && subCertFile != "" { subTLS = true } if subDomain == "" { subDomain = extractHostname(host) } if subTLS { subURI = "https://" } else { subURI = "http://" } if (subPort == 443 && subTLS) || (subPort == 80 && !subTLS) { subURI += subDomain } else { subURI += fmt.Sprintf("%s:%d", subDomain, subPort) } if subEnable && result["subURI"].(string) == "" { result["subURI"] = subURI + subPath } if result["subTitle"].(string) == "" { result["subTitle"] = subTitle } if subJsonEnable && result["subJsonURI"].(string) == "" { result["subJsonURI"] = subURI + subJsonPath } } return result, nil } ================================================ FILE: web/service/tgbot.go ================================================ package service import ( "context" "crypto/rand" "embed" "encoding/base64" "encoding/json" "errors" "fmt" "html" "io" "math/big" "net" "net/http" "net/url" "os" "regexp" "slices" "strconv" "strings" "sync" "time" "github.com/mhsanaei/3x-ui/v2/config" "github.com/mhsanaei/3x-ui/v2/database" "github.com/mhsanaei/3x-ui/v2/database/model" "github.com/mhsanaei/3x-ui/v2/logger" "github.com/mhsanaei/3x-ui/v2/util/common" "github.com/mhsanaei/3x-ui/v2/web/global" "github.com/mhsanaei/3x-ui/v2/web/locale" "github.com/mhsanaei/3x-ui/v2/xray" "github.com/google/uuid" "github.com/mymmrac/telego" th "github.com/mymmrac/telego/telegohandler" tu "github.com/mymmrac/telego/telegoutil" "github.com/skip2/go-qrcode" "github.com/valyala/fasthttp" "github.com/valyala/fasthttp/fasthttpproxy" ) var ( bot *telego.Bot // botCancel stores the function to cancel the context, stopping Long Polling gracefully. botCancel context.CancelFunc // tgBotMutex protects concurrent access to botCancel variable tgBotMutex sync.Mutex // botWG waits for the OnReceive Long Polling goroutine to finish. botWG sync.WaitGroup botHandler *th.BotHandler adminIds []int64 isRunning bool hostname string hashStorage *global.HashStorage // Performance improvements messageWorkerPool chan struct{} // Semaphore for limiting concurrent message processing optimizedHTTPClient *http.Client // HTTP client with connection pooling and timeouts // Simple cache for frequently accessed data statusCache struct { data *Status timestamp time.Time mutex sync.RWMutex } serverStatsCache struct { data string timestamp time.Time mutex sync.RWMutex } // clients data to adding new client receiver_inbound_ID int client_Id string client_Flow string client_Email string client_LimitIP int client_TotalGB int64 client_ExpiryTime int64 client_Enable bool client_TgID string client_SubID string client_Comment string client_Reset int client_Security string client_ShPassword string client_TrPassword string client_Method string ) var userStates = make(map[int64]string) // LoginStatus represents the result of a login attempt. type LoginStatus byte // Login status constants const ( LoginSuccess LoginStatus = 1 // Login was successful LoginFail LoginStatus = 0 // Login failed EmptyTelegramUserID = int64(0) // Default value for empty Telegram user ID ) // Tgbot provides business logic for Telegram bot integration. // It handles bot commands, user interactions, and status reporting via Telegram. type Tgbot struct { inboundService InboundService settingService SettingService serverService ServerService xrayService XrayService lastStatus *Status } // NewTgbot creates a new Tgbot instance. func (t *Tgbot) NewTgbot() *Tgbot { return new(Tgbot) } // I18nBot retrieves a localized message for the bot interface. func (t *Tgbot) I18nBot(name string, params ...string) string { return locale.I18n(locale.Bot, name, params...) } // GetHashStorage returns the hash storage instance for callback queries. func (t *Tgbot) GetHashStorage() *global.HashStorage { return hashStorage } // getCachedStatus returns cached server status if it's fresh enough (less than 5 seconds old) func (t *Tgbot) getCachedStatus() (*Status, bool) { statusCache.mutex.RLock() defer statusCache.mutex.RUnlock() if statusCache.data != nil && time.Since(statusCache.timestamp) < 5*time.Second { return statusCache.data, true } return nil, false } // setCachedStatus updates the status cache func (t *Tgbot) setCachedStatus(status *Status) { statusCache.mutex.Lock() defer statusCache.mutex.Unlock() statusCache.data = status statusCache.timestamp = time.Now() } // getCachedServerStats returns cached server stats if it's fresh enough (less than 10 seconds old) func (t *Tgbot) getCachedServerStats() (string, bool) { serverStatsCache.mutex.RLock() defer serverStatsCache.mutex.RUnlock() if serverStatsCache.data != "" && time.Since(serverStatsCache.timestamp) < 10*time.Second { return serverStatsCache.data, true } return "", false } // setCachedServerStats updates the server stats cache func (t *Tgbot) setCachedServerStats(stats string) { serverStatsCache.mutex.Lock() defer serverStatsCache.mutex.Unlock() serverStatsCache.data = stats serverStatsCache.timestamp = time.Now() } // Start initializes and starts the Telegram bot with the provided translation files. func (t *Tgbot) Start(i18nFS embed.FS) error { // Initialize localizer err := locale.InitLocalizer(i18nFS, &t.settingService) if err != nil { return err } // If Start is called again (e.g. during reload), ensure any previous long-polling // loop is stopped before creating a new bot / receiver. StopBot() // Initialize hash storage to store callback queries hashStorage = global.NewHashStorage(20 * time.Minute) // Initialize worker pool for concurrent message processing (max 10 concurrent handlers) messageWorkerPool = make(chan struct{}, 10) // Initialize optimized HTTP client with connection pooling optimizedHTTPClient = &http.Client{ Timeout: 15 * time.Second, Transport: &http.Transport{ MaxIdleConns: 100, MaxIdleConnsPerHost: 10, IdleConnTimeout: 30 * time.Second, DisableKeepAlives: false, }, } t.SetHostname() // Get Telegram bot token tgBotToken, err := t.settingService.GetTgBotToken() if err != nil || tgBotToken == "" { logger.Warning("Failed to get Telegram bot token:", err) return err } // Get Telegram bot chat ID(s) tgBotID, err := t.settingService.GetTgBotChatId() if err != nil { logger.Warning("Failed to get Telegram bot chat ID:", err) return err } parsedAdminIds := make([]int64, 0) // Parse admin IDs from comma-separated string if tgBotID != "" { for _, adminID := range strings.Split(tgBotID, ",") { id, err := strconv.ParseInt(adminID, 10, 64) if err != nil { logger.Warning("Failed to parse admin ID from Telegram bot chat ID:", err) return err } parsedAdminIds = append(parsedAdminIds, int64(id)) } } tgBotMutex.Lock() adminIds = parsedAdminIds tgBotMutex.Unlock() // Get Telegram bot proxy URL tgBotProxy, err := t.settingService.GetTgBotProxy() if err != nil { logger.Warning("Failed to get Telegram bot proxy URL:", err) } // Get Telegram bot API server URL tgBotAPIServer, err := t.settingService.GetTgBotAPIServer() if err != nil { logger.Warning("Failed to get Telegram bot API server URL:", err) } // Create new Telegram bot instance bot, err = t.NewBot(tgBotToken, tgBotProxy, tgBotAPIServer) if err != nil { logger.Error("Failed to initialize Telegram bot API:", err) return err } // After bot initialization, set up bot commands with localized descriptions err = bot.SetMyCommands(context.Background(), &telego.SetMyCommandsParams{ Commands: []telego.BotCommand{ {Command: "start", Description: t.I18nBot("tgbot.commands.startDesc")}, {Command: "help", Description: t.I18nBot("tgbot.commands.helpDesc")}, {Command: "status", Description: t.I18nBot("tgbot.commands.statusDesc")}, {Command: "id", Description: t.I18nBot("tgbot.commands.idDesc")}, }, }) if err != nil { logger.Warning("Failed to set bot commands:", err) } // Start receiving Telegram bot messages tgBotMutex.Lock() alreadyRunning := isRunning || botCancel != nil tgBotMutex.Unlock() if !alreadyRunning { logger.Info("Telegram bot receiver started") go t.OnReceive() } return nil } // createRobustFastHTTPClient creates a fasthttp.Client with proper connection handling func (t *Tgbot) createRobustFastHTTPClient(proxyUrl string) *fasthttp.Client { client := &fasthttp.Client{ // Connection timeouts ReadTimeout: 30 * time.Second, WriteTimeout: 30 * time.Second, MaxIdleConnDuration: 60 * time.Second, MaxConnDuration: 0, // unlimited, but controlled by MaxIdleConnDuration MaxIdemponentCallAttempts: 3, ReadBufferSize: 4096, WriteBufferSize: 4096, MaxConnsPerHost: 100, MaxConnWaitTimeout: 10 * time.Second, DisableHeaderNamesNormalizing: false, DisablePathNormalizing: false, // Retry on connection errors RetryIf: func(request *fasthttp.Request) bool { // Retry on connection errors for GET requests return string(request.Header.Method()) == "GET" || string(request.Header.Method()) == "POST" }, } // Set proxy if provided if proxyUrl != "" { client.Dial = fasthttpproxy.FasthttpSocksDialer(proxyUrl) } return client } // NewBot creates a new Telegram bot instance with optional proxy and API server settings. func (t *Tgbot) NewBot(token string, proxyUrl string, apiServerUrl string) (*telego.Bot, error) { // Validate proxy URL if provided if proxyUrl != "" { if !strings.HasPrefix(proxyUrl, "socks5://") { logger.Warning("Invalid socks5 URL, ignoring proxy") proxyUrl = "" // Clear invalid proxy } else { _, err := url.Parse(proxyUrl) if err != nil { logger.Warningf("Can't parse proxy URL, ignoring proxy: %v", err) proxyUrl = "" } } } // Validate API server URL if provided if apiServerUrl != "" { if !strings.HasPrefix(apiServerUrl, "http") { logger.Warning("Invalid http(s) URL for API server, using default") apiServerUrl = "" } else { _, err := url.Parse(apiServerUrl) if err != nil { logger.Warningf("Can't parse API server URL, using default: %v", err) apiServerUrl = "" } } } // Create robust fasthttp client client := t.createRobustFastHTTPClient(proxyUrl) // Build bot options var options []telego.BotOption options = append(options, telego.WithFastHTTPClient(client)) if apiServerUrl != "" { options = append(options, telego.WithAPIServer(apiServerUrl)) } return telego.NewBot(token, options...) } // IsRunning checks if the Telegram bot is currently running. func (t *Tgbot) IsRunning() bool { tgBotMutex.Lock() defer tgBotMutex.Unlock() return isRunning } // SetHostname sets the hostname for the bot. func (t *Tgbot) SetHostname() { host, err := os.Hostname() if err != nil { logger.Error("get hostname error:", err) hostname = "" return } hostname = host } // Stop safely stops the Telegram bot's Long Polling operation. // This method now calls the global StopBot function and cleans up other resources. func (t *Tgbot) Stop() { StopBot() logger.Info("Stop Telegram receiver ...") tgBotMutex.Lock() adminIds = nil tgBotMutex.Unlock() } // StopBot safely stops the Telegram bot's Long Polling operation by cancelling its context. // This is the global function called from main.go's signal handler and t.Stop(). func StopBot() { // Don't hold the mutex while cancelling/waiting. tgBotMutex.Lock() cancel := botCancel botCancel = nil handler := botHandler botHandler = nil isRunning = false tgBotMutex.Unlock() if handler != nil { handler.Stop() } if cancel != nil { logger.Info("Sending cancellation signal to Telegram bot...") // Cancels the context passed to UpdatesViaLongPolling; this closes updates channel // and lets botHandler.Start() exit cleanly. cancel() botWG.Wait() logger.Info("Telegram bot successfully stopped.") } } // encodeQuery encodes the query string if it's longer than 64 characters. func (t *Tgbot) encodeQuery(query string) string { // NOTE: we only need to hash for more than 64 chars if len(query) <= 64 { return query } return hashStorage.SaveHash(query) } // decodeQuery decodes a hashed query string back to its original form. func (t *Tgbot) decodeQuery(query string) (string, error) { if !hashStorage.IsMD5(query) { return query, nil } decoded, exists := hashStorage.GetValue(query) if !exists { return "", common.NewError("hash not found in storage!") } return decoded, nil } // OnReceive starts the message receiving loop for the Telegram bot. func (t *Tgbot) OnReceive() { params := telego.GetUpdatesParams{ Timeout: 20, // Reduced timeout to detect connection issues faster } // Strict singleton: never start a second long-polling loop. tgBotMutex.Lock() if botCancel != nil || isRunning { tgBotMutex.Unlock() logger.Warning("TgBot OnReceive called while already running; ignoring.") return } ctx, cancel := context.WithCancel(context.Background()) botCancel = cancel isRunning = true // Add to WaitGroup before releasing the lock so StopBot() can't return // before this receiver goroutine is accounted for. botWG.Add(1) tgBotMutex.Unlock() // Get updates channel using the context with shorter timeout for better error recovery updates, _ := bot.UpdatesViaLongPolling(ctx, ¶ms) go func() { defer botWG.Done() h, _ := th.NewBotHandler(bot, updates) tgBotMutex.Lock() botHandler = h tgBotMutex.Unlock() h.HandleMessage(func(ctx *th.Context, message telego.Message) error { delete(userStates, message.Chat.ID) t.SendMsgToTgbot(message.Chat.ID, t.I18nBot("tgbot.keyboardClosed"), tu.ReplyKeyboardRemove()) return nil }, th.TextEqual(t.I18nBot("tgbot.buttons.closeKeyboard"))) h.HandleMessage(func(ctx *th.Context, message telego.Message) error { // Use goroutine with worker pool for concurrent command processing go func() { messageWorkerPool <- struct{}{} // Acquire worker defer func() { <-messageWorkerPool }() // Release worker delete(userStates, message.Chat.ID) t.answerCommand(&message, message.Chat.ID, checkAdmin(message.From.ID)) }() return nil }, th.AnyCommand()) h.HandleCallbackQuery(func(ctx *th.Context, query telego.CallbackQuery) error { // Use goroutine with worker pool for concurrent callback processing go func() { messageWorkerPool <- struct{}{} // Acquire worker defer func() { <-messageWorkerPool }() // Release worker delete(userStates, query.Message.GetChat().ID) t.answerCallback(&query, checkAdmin(query.From.ID)) }() return nil }, th.AnyCallbackQueryWithMessage()) h.HandleMessage(func(ctx *th.Context, message telego.Message) error { if userState, exists := userStates[message.Chat.ID]; exists { switch userState { case "awaiting_id": if client_Id == strings.TrimSpace(message.Text) { t.SendMsgToTgbotDeleteAfter(message.Chat.ID, t.I18nBot("tgbot.messages.using_default_value"), 3, tu.ReplyKeyboardRemove()) delete(userStates, message.Chat.ID) inbound, _ := t.inboundService.GetInbound(receiver_inbound_ID) message_text, _ := t.BuildInboundClientDataMessage(inbound.Remark, inbound.Protocol) t.addClient(message.Chat.ID, message_text) return nil } client_Id = strings.TrimSpace(message.Text) if t.isSingleWord(client_Id) { userStates[message.Chat.ID] = "awaiting_id" cancel_btn_markup := tu.InlineKeyboard( tu.InlineKeyboardRow( tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.use_default")).WithCallbackData("add_client_default_info"), ), ) t.SendMsgToTgbot(message.Chat.ID, t.I18nBot("tgbot.messages.incorrect_input"), cancel_btn_markup) } else { t.SendMsgToTgbotDeleteAfter(message.Chat.ID, t.I18nBot("tgbot.messages.received_id"), 3, tu.ReplyKeyboardRemove()) delete(userStates, message.Chat.ID) inbound, _ := t.inboundService.GetInbound(receiver_inbound_ID) message_text, _ := t.BuildInboundClientDataMessage(inbound.Remark, inbound.Protocol) t.addClient(message.Chat.ID, message_text) } case "awaiting_password_tr": if client_TrPassword == strings.TrimSpace(message.Text) { t.SendMsgToTgbotDeleteAfter(message.Chat.ID, t.I18nBot("tgbot.messages.using_default_value"), 3, tu.ReplyKeyboardRemove()) delete(userStates, message.Chat.ID) return nil } client_TrPassword = strings.TrimSpace(message.Text) if t.isSingleWord(client_TrPassword) { userStates[message.Chat.ID] = "awaiting_password_tr" cancel_btn_markup := tu.InlineKeyboard( tu.InlineKeyboardRow( tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.use_default")).WithCallbackData("add_client_default_info"), ), ) t.SendMsgToTgbot(message.Chat.ID, t.I18nBot("tgbot.messages.incorrect_input"), cancel_btn_markup) } else { t.SendMsgToTgbotDeleteAfter(message.Chat.ID, t.I18nBot("tgbot.messages.received_password"), 3, tu.ReplyKeyboardRemove()) delete(userStates, message.Chat.ID) inbound, _ := t.inboundService.GetInbound(receiver_inbound_ID) message_text, _ := t.BuildInboundClientDataMessage(inbound.Remark, inbound.Protocol) t.addClient(message.Chat.ID, message_text) } case "awaiting_password_sh": if client_ShPassword == strings.TrimSpace(message.Text) { t.SendMsgToTgbotDeleteAfter(message.Chat.ID, t.I18nBot("tgbot.messages.using_default_value"), 3, tu.ReplyKeyboardRemove()) delete(userStates, message.Chat.ID) return nil } client_ShPassword = strings.TrimSpace(message.Text) if t.isSingleWord(client_ShPassword) { userStates[message.Chat.ID] = "awaiting_password_sh" cancel_btn_markup := tu.InlineKeyboard( tu.InlineKeyboardRow( tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.use_default")).WithCallbackData("add_client_default_info"), ), ) t.SendMsgToTgbot(message.Chat.ID, t.I18nBot("tgbot.messages.incorrect_input"), cancel_btn_markup) } else { t.SendMsgToTgbotDeleteAfter(message.Chat.ID, t.I18nBot("tgbot.messages.received_password"), 3, tu.ReplyKeyboardRemove()) delete(userStates, message.Chat.ID) inbound, _ := t.inboundService.GetInbound(receiver_inbound_ID) message_text, _ := t.BuildInboundClientDataMessage(inbound.Remark, inbound.Protocol) t.addClient(message.Chat.ID, message_text) } case "awaiting_email": if client_Email == strings.TrimSpace(message.Text) { t.SendMsgToTgbotDeleteAfter(message.Chat.ID, t.I18nBot("tgbot.messages.using_default_value"), 3, tu.ReplyKeyboardRemove()) delete(userStates, message.Chat.ID) return nil } client_Email = strings.TrimSpace(message.Text) if t.isSingleWord(client_Email) { userStates[message.Chat.ID] = "awaiting_email" cancel_btn_markup := tu.InlineKeyboard( tu.InlineKeyboardRow( tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.use_default")).WithCallbackData("add_client_default_info"), ), ) t.SendMsgToTgbot(message.Chat.ID, t.I18nBot("tgbot.messages.incorrect_input"), cancel_btn_markup) } else { t.SendMsgToTgbotDeleteAfter(message.Chat.ID, t.I18nBot("tgbot.messages.received_email"), 3, tu.ReplyKeyboardRemove()) delete(userStates, message.Chat.ID) inbound, _ := t.inboundService.GetInbound(receiver_inbound_ID) message_text, _ := t.BuildInboundClientDataMessage(inbound.Remark, inbound.Protocol) t.addClient(message.Chat.ID, message_text) } case "awaiting_comment": if client_Comment == strings.TrimSpace(message.Text) { t.SendMsgToTgbotDeleteAfter(message.Chat.ID, t.I18nBot("tgbot.messages.using_default_value"), 3, tu.ReplyKeyboardRemove()) delete(userStates, message.Chat.ID) return nil } client_Comment = strings.TrimSpace(message.Text) t.SendMsgToTgbotDeleteAfter(message.Chat.ID, t.I18nBot("tgbot.messages.received_comment"), 3, tu.ReplyKeyboardRemove()) delete(userStates, message.Chat.ID) inbound, _ := t.inboundService.GetInbound(receiver_inbound_ID) message_text, _ := t.BuildInboundClientDataMessage(inbound.Remark, inbound.Protocol) t.addClient(message.Chat.ID, message_text) } } else { if message.UsersShared != nil { if checkAdmin(message.From.ID) { for _, sharedUser := range message.UsersShared.Users { userID := sharedUser.UserID needRestart, err := t.inboundService.SetClientTelegramUserID(message.UsersShared.RequestID, userID) if needRestart { t.xrayService.SetToNeedRestart() } output := "" if err != nil { output += t.I18nBot("tgbot.messages.selectUserFailed") } else { output += t.I18nBot("tgbot.messages.userSaved") } t.SendMsgToTgbot(message.Chat.ID, output, tu.ReplyKeyboardRemove()) } } else { t.SendMsgToTgbot(message.Chat.ID, t.I18nBot("tgbot.noResult"), tu.ReplyKeyboardRemove()) } } } return nil }, th.AnyMessage()) h.Start() }() } // answerCommand processes incoming command messages from Telegram users. func (t *Tgbot) answerCommand(message *telego.Message, chatId int64, isAdmin bool) { msg, onlyMessage := "", false command, _, commandArgs := tu.ParseCommand(message.Text) // Helper function to handle unknown commands. handleUnknownCommand := func() { msg += t.I18nBot("tgbot.commands.unknown") } // Handle the command. switch command { case "help": msg += t.I18nBot("tgbot.commands.help") msg += t.I18nBot("tgbot.commands.pleaseChoose") case "start": msg += t.I18nBot("tgbot.commands.start", "Firstname=="+html.EscapeString(message.From.FirstName)) if isAdmin { msg += t.I18nBot("tgbot.commands.welcome", "Hostname=="+hostname) } msg += "\n\n" + t.I18nBot("tgbot.commands.pleaseChoose") case "status": onlyMessage = true msg += t.I18nBot("tgbot.commands.status") case "id": onlyMessage = true msg += t.I18nBot("tgbot.commands.getID", "ID=="+strconv.FormatInt(message.From.ID, 10)) case "usage": onlyMessage = true if len(commandArgs) > 0 { if isAdmin { t.searchClient(chatId, commandArgs[0]) } else { t.getClientUsage(chatId, int64(message.From.ID), commandArgs[0]) } } else { msg += t.I18nBot("tgbot.commands.usage") } case "inbound": onlyMessage = true if isAdmin && len(commandArgs) > 0 { t.searchInbound(chatId, commandArgs[0]) } else { handleUnknownCommand() } case "restart": onlyMessage = true if isAdmin { if len(commandArgs) == 0 { if t.xrayService.IsXrayRunning() { err := t.xrayService.RestartXray(true) if err != nil { msg += t.I18nBot("tgbot.commands.restartFailed", "Error=="+err.Error()) } else { msg += t.I18nBot("tgbot.commands.restartSuccess") } } else { msg += t.I18nBot("tgbot.commands.xrayNotRunning") } } else { handleUnknownCommand() msg += t.I18nBot("tgbot.commands.restartUsage") } } else { handleUnknownCommand() } default: handleUnknownCommand() } if msg != "" { t.sendResponse(chatId, msg, onlyMessage, isAdmin) } } // sendResponse sends the response message based on the onlyMessage flag. func (t *Tgbot) sendResponse(chatId int64, msg string, onlyMessage, isAdmin bool) { if onlyMessage { t.SendMsgToTgbot(chatId, msg) } else { t.SendAnswer(chatId, msg, isAdmin) } } // randomLowerAndNum generates a random string of lowercase letters and numbers. func (t *Tgbot) randomLowerAndNum(length int) string { charset := "abcdefghijklmnopqrstuvwxyz0123456789" bytes := make([]byte, length) for i := range bytes { randomIndex, _ := rand.Int(rand.Reader, big.NewInt(int64(len(charset)))) bytes[i] = charset[randomIndex.Int64()] } return string(bytes) } // randomShadowSocksPassword generates a random password for Shadowsocks. func (t *Tgbot) randomShadowSocksPassword() string { array := make([]byte, 32) _, err := rand.Read(array) if err != nil { return t.randomLowerAndNum(32) } return base64.StdEncoding.EncodeToString(array) } // answerCallback processes callback queries from inline keyboards. func (t *Tgbot) answerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool) { chatId := callbackQuery.Message.GetChat().ID if isAdmin { // get query from hash storage decodedQuery, err := t.decodeQuery(callbackQuery.Data) if err != nil { t.SendMsgToTgbot(chatId, t.I18nBot("tgbot.noQuery")) return } dataArray := strings.Split(decodedQuery, " ") if len(dataArray) >= 2 && len(dataArray[1]) > 0 { email := dataArray[1] switch dataArray[0] { case "get_clients_for_sub": inboundId := dataArray[1] inboundIdInt, err := strconv.Atoi(inboundId) if err != nil { t.sendCallbackAnswerTgBot(callbackQuery.ID, err.Error()) return } clientsKB, err := t.getInboundClientsFor(inboundIdInt, "client_sub_links") if err != nil { t.sendCallbackAnswerTgBot(callbackQuery.ID, err.Error()) return } inbound, _ := t.inboundService.GetInbound(inboundIdInt) t.SendMsgToTgbot(chatId, t.I18nBot("tgbot.answers.chooseClient", "Inbound=="+inbound.Remark), clientsKB) case "get_clients_for_individual": inboundId := dataArray[1] inboundIdInt, err := strconv.Atoi(inboundId) if err != nil { t.sendCallbackAnswerTgBot(callbackQuery.ID, err.Error()) return } clientsKB, err := t.getInboundClientsFor(inboundIdInt, "client_individual_links") if err != nil { t.sendCallbackAnswerTgBot(callbackQuery.ID, err.Error()) return } inbound, _ := t.inboundService.GetInbound(inboundIdInt) t.SendMsgToTgbot(chatId, t.I18nBot("tgbot.answers.chooseClient", "Inbound=="+inbound.Remark), clientsKB) case "get_clients_for_qr": inboundId := dataArray[1] inboundIdInt, err := strconv.Atoi(inboundId) if err != nil { t.sendCallbackAnswerTgBot(callbackQuery.ID, err.Error()) return } clientsKB, err := t.getInboundClientsFor(inboundIdInt, "client_qr_links") if err != nil { t.sendCallbackAnswerTgBot(callbackQuery.ID, err.Error()) return } inbound, _ := t.inboundService.GetInbound(inboundIdInt) t.SendMsgToTgbot(chatId, t.I18nBot("tgbot.answers.chooseClient", "Inbound=="+inbound.Remark), clientsKB) case "client_sub_links": t.sendClientSubLinks(chatId, email) return case "client_individual_links": t.sendClientIndividualLinks(chatId, email) return case "client_qr_links": t.sendClientQRLinks(chatId, email) return case "client_get_usage": t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.messages.email", "Email=="+email)) t.searchClient(chatId, email) case "client_refresh": t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.clientRefreshSuccess", "Email=="+email)) t.searchClient(chatId, email, callbackQuery.Message.GetMessageID()) case "client_cancel": t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.canceled", "Email=="+email)) t.searchClient(chatId, email, callbackQuery.Message.GetMessageID()) case "ips_refresh": t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.IpRefreshSuccess", "Email=="+email)) t.searchClientIps(chatId, email, callbackQuery.Message.GetMessageID()) case "ips_cancel": t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.canceled", "Email=="+email)) t.searchClientIps(chatId, email, callbackQuery.Message.GetMessageID()) case "tgid_refresh": t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.TGIdRefreshSuccess", "Email=="+email)) t.clientTelegramUserInfo(chatId, email, callbackQuery.Message.GetMessageID()) case "tgid_cancel": t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.canceled", "Email=="+email)) t.clientTelegramUserInfo(chatId, email, callbackQuery.Message.GetMessageID()) case "reset_traffic": inlineKeyboard := tu.InlineKeyboard( tu.InlineKeyboardRow( tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.cancelReset")).WithCallbackData(t.encodeQuery("client_cancel "+email)), ), tu.InlineKeyboardRow( tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.confirmResetTraffic")).WithCallbackData(t.encodeQuery("reset_traffic_c "+email)), ), ) t.editMessageCallbackTgBot(chatId, callbackQuery.Message.GetMessageID(), inlineKeyboard) case "reset_traffic_c": err := t.inboundService.ResetClientTrafficByEmail(email) if err == nil { t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.resetTrafficSuccess", "Email=="+email)) t.searchClient(chatId, email, callbackQuery.Message.GetMessageID()) } else { t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.errorOperation")) } case "limit_traffic": inlineKeyboard := tu.InlineKeyboard( tu.InlineKeyboardRow( tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.cancel")).WithCallbackData(t.encodeQuery("client_cancel "+email)), ), tu.InlineKeyboardRow( tu.InlineKeyboardButton(t.I18nBot("tgbot.unlimited")).WithCallbackData(t.encodeQuery("limit_traffic_c "+email+" 0")), tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.custom")).WithCallbackData(t.encodeQuery("limit_traffic_in "+email+" 0")), ), tu.InlineKeyboardRow( tu.InlineKeyboardButton("1 GB").WithCallbackData(t.encodeQuery("limit_traffic_c "+email+" 1")), tu.InlineKeyboardButton("5 GB").WithCallbackData(t.encodeQuery("limit_traffic_c "+email+" 5")), tu.InlineKeyboardButton("10 GB").WithCallbackData(t.encodeQuery("limit_traffic_c "+email+" 10")), ), tu.InlineKeyboardRow( tu.InlineKeyboardButton("20 GB").WithCallbackData(t.encodeQuery("limit_traffic_c "+email+" 20")), tu.InlineKeyboardButton("30 GB").WithCallbackData(t.encodeQuery("limit_traffic_c "+email+" 30")), tu.InlineKeyboardButton("40 GB").WithCallbackData(t.encodeQuery("limit_traffic_c "+email+" 40")), ), tu.InlineKeyboardRow( tu.InlineKeyboardButton("50 GB").WithCallbackData(t.encodeQuery("limit_traffic_c "+email+" 50")), tu.InlineKeyboardButton("60 GB").WithCallbackData(t.encodeQuery("limit_traffic_c "+email+" 60")), tu.InlineKeyboardButton("80 GB").WithCallbackData(t.encodeQuery("limit_traffic_c "+email+" 80")), ), tu.InlineKeyboardRow( tu.InlineKeyboardButton("100 GB").WithCallbackData(t.encodeQuery("limit_traffic_c "+email+" 100")), tu.InlineKeyboardButton("150 GB").WithCallbackData(t.encodeQuery("limit_traffic_c "+email+" 150")), tu.InlineKeyboardButton("200 GB").WithCallbackData(t.encodeQuery("limit_traffic_c "+email+" 200")), ), ) t.editMessageCallbackTgBot(chatId, callbackQuery.Message.GetMessageID(), inlineKeyboard) case "limit_traffic_c": if len(dataArray) == 3 { limitTraffic, err := strconv.Atoi(dataArray[2]) if err == nil { needRestart, err := t.inboundService.ResetClientTrafficLimitByEmail(email, limitTraffic) if needRestart { t.xrayService.SetToNeedRestart() } if err == nil { t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.setTrafficLimitSuccess", "Email=="+email)) t.searchClient(chatId, email, callbackQuery.Message.GetMessageID()) return } } } t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.errorOperation")) t.searchClient(chatId, email, callbackQuery.Message.GetMessageID()) case "limit_traffic_in": if len(dataArray) >= 3 { oldInputNumber, err := strconv.Atoi(dataArray[2]) inputNumber := oldInputNumber if err == nil { if len(dataArray) == 4 { num, err := strconv.Atoi(dataArray[3]) if err == nil { switch num { case -2: inputNumber = 0 case -1: if inputNumber > 0 { inputNumber = (inputNumber / 10) } default: inputNumber = (inputNumber * 10) + num } } if inputNumber == oldInputNumber { t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.successfulOperation")) return } if inputNumber >= 999999 { t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.errorOperation")) return } } inlineKeyboard := tu.InlineKeyboard( tu.InlineKeyboardRow( tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.cancel")).WithCallbackData(t.encodeQuery("client_cancel "+email)), ), tu.InlineKeyboardRow( tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.confirmNumberAdd", "Num=="+strconv.Itoa(inputNumber))).WithCallbackData(t.encodeQuery("limit_traffic_c "+email+" "+strconv.Itoa(inputNumber))), ), tu.InlineKeyboardRow( tu.InlineKeyboardButton("1").WithCallbackData(t.encodeQuery("limit_traffic_in "+email+" "+strconv.Itoa(inputNumber)+" 1")), tu.InlineKeyboardButton("2").WithCallbackData(t.encodeQuery("limit_traffic_in "+email+" "+strconv.Itoa(inputNumber)+" 2")), tu.InlineKeyboardButton("3").WithCallbackData(t.encodeQuery("limit_traffic_in "+email+" "+strconv.Itoa(inputNumber)+" 3")), ), tu.InlineKeyboardRow( tu.InlineKeyboardButton("4").WithCallbackData(t.encodeQuery("limit_traffic_in "+email+" "+strconv.Itoa(inputNumber)+" 4")), tu.InlineKeyboardButton("5").WithCallbackData(t.encodeQuery("limit_traffic_in "+email+" "+strconv.Itoa(inputNumber)+" 5")), tu.InlineKeyboardButton("6").WithCallbackData(t.encodeQuery("limit_traffic_in "+email+" "+strconv.Itoa(inputNumber)+" 6")), ), tu.InlineKeyboardRow( tu.InlineKeyboardButton("7").WithCallbackData(t.encodeQuery("limit_traffic_in "+email+" "+strconv.Itoa(inputNumber)+" 7")), tu.InlineKeyboardButton("8").WithCallbackData(t.encodeQuery("limit_traffic_in "+email+" "+strconv.Itoa(inputNumber)+" 8")), tu.InlineKeyboardButton("9").WithCallbackData(t.encodeQuery("limit_traffic_in "+email+" "+strconv.Itoa(inputNumber)+" 9")), ), tu.InlineKeyboardRow( tu.InlineKeyboardButton("🔄").WithCallbackData(t.encodeQuery("limit_traffic_in "+email+" "+strconv.Itoa(inputNumber)+" -2")), tu.InlineKeyboardButton("0").WithCallbackData(t.encodeQuery("limit_traffic_in "+email+" "+strconv.Itoa(inputNumber)+" 0")), tu.InlineKeyboardButton("⬅️").WithCallbackData(t.encodeQuery("limit_traffic_in "+email+" "+strconv.Itoa(inputNumber)+" -1")), ), ) t.editMessageCallbackTgBot(chatId, callbackQuery.Message.GetMessageID(), inlineKeyboard) return } } t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.errorOperation")) t.searchClient(chatId, email, callbackQuery.Message.GetMessageID()) case "add_client_limit_traffic_c": limitTraffic, _ := strconv.ParseInt(dataArray[1], 10, 64) client_TotalGB = limitTraffic * 1024 * 1024 * 1024 messageId := callbackQuery.Message.GetMessageID() inbound, err := t.inboundService.GetInbound(receiver_inbound_ID) if err != nil { t.sendCallbackAnswerTgBot(callbackQuery.ID, err.Error()) return } message_text, err := t.BuildInboundClientDataMessage(inbound.Remark, inbound.Protocol) if err != nil { t.sendCallbackAnswerTgBot(callbackQuery.ID, err.Error()) return } t.addClient(callbackQuery.Message.GetChat().ID, message_text, messageId) t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.successfulOperation")) case "add_client_limit_traffic_in": if len(dataArray) >= 2 { oldInputNumber, err := strconv.Atoi(dataArray[1]) inputNumber := oldInputNumber if err == nil { if len(dataArray) == 3 { num, err := strconv.Atoi(dataArray[2]) if err == nil { switch num { case -2: inputNumber = 0 case -1: if inputNumber > 0 { inputNumber = (inputNumber / 10) } default: inputNumber = (inputNumber * 10) + num } } if inputNumber == oldInputNumber { t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.successfulOperation")) return } if inputNumber >= 999999 { t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.errorOperation")) return } } inlineKeyboard := tu.InlineKeyboard( tu.InlineKeyboardRow( tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.cancel")).WithCallbackData(t.encodeQuery("add_client_default_traffic_exp")), ), tu.InlineKeyboardRow( tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.confirmNumberAdd", "Num=="+strconv.Itoa(inputNumber))).WithCallbackData(t.encodeQuery("add_client_limit_traffic_c "+strconv.Itoa(inputNumber))), ), tu.InlineKeyboardRow( tu.InlineKeyboardButton("1").WithCallbackData(t.encodeQuery("add_client_limit_traffic_in "+strconv.Itoa(inputNumber)+" 1")), tu.InlineKeyboardButton("2").WithCallbackData(t.encodeQuery("add_client_limit_traffic_in "+strconv.Itoa(inputNumber)+" 2")), tu.InlineKeyboardButton("3").WithCallbackData(t.encodeQuery("add_client_limit_traffic_in "+strconv.Itoa(inputNumber)+" 3")), ), tu.InlineKeyboardRow( tu.InlineKeyboardButton("4").WithCallbackData(t.encodeQuery("add_client_limit_traffic_in "+strconv.Itoa(inputNumber)+" 4")), tu.InlineKeyboardButton("5").WithCallbackData(t.encodeQuery("add_client_limit_traffic_in "+strconv.Itoa(inputNumber)+" 5")), tu.InlineKeyboardButton("6").WithCallbackData(t.encodeQuery("add_client_limit_traffic_in "+strconv.Itoa(inputNumber)+" 6")), ), tu.InlineKeyboardRow( tu.InlineKeyboardButton("7").WithCallbackData(t.encodeQuery("add_client_limit_traffic_in "+strconv.Itoa(inputNumber)+" 7")), tu.InlineKeyboardButton("8").WithCallbackData(t.encodeQuery("add_client_limit_traffic_in "+strconv.Itoa(inputNumber)+" 8")), tu.InlineKeyboardButton("9").WithCallbackData(t.encodeQuery("add_client_limit_traffic_in "+strconv.Itoa(inputNumber)+" 9")), ), tu.InlineKeyboardRow( tu.InlineKeyboardButton("🔄").WithCallbackData(t.encodeQuery("add_client_limit_traffic_in "+strconv.Itoa(inputNumber)+" -2")), tu.InlineKeyboardButton("0").WithCallbackData(t.encodeQuery("add_client_limit_traffic_in "+strconv.Itoa(inputNumber)+" 0")), tu.InlineKeyboardButton("⬅️").WithCallbackData(t.encodeQuery("add_client_limit_traffic_in "+strconv.Itoa(inputNumber)+" -1")), ), ) t.editMessageCallbackTgBot(chatId, callbackQuery.Message.GetMessageID(), inlineKeyboard) return } } case "reset_exp": inlineKeyboard := tu.InlineKeyboard( tu.InlineKeyboardRow( tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.cancelReset")).WithCallbackData(t.encodeQuery("client_cancel "+email)), ), tu.InlineKeyboardRow( tu.InlineKeyboardButton(t.I18nBot("tgbot.unlimited")).WithCallbackData(t.encodeQuery("reset_exp_c "+email+" 0")), tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.custom")).WithCallbackData(t.encodeQuery("reset_exp_in "+email+" 0")), ), tu.InlineKeyboardRow( tu.InlineKeyboardButton(t.I18nBot("tgbot.add")+" 7 "+t.I18nBot("tgbot.days")).WithCallbackData(t.encodeQuery("reset_exp_c "+email+" 7")), tu.InlineKeyboardButton(t.I18nBot("tgbot.add")+" 10 "+t.I18nBot("tgbot.days")).WithCallbackData(t.encodeQuery("reset_exp_c "+email+" 10")), ), tu.InlineKeyboardRow( tu.InlineKeyboardButton(t.I18nBot("tgbot.add")+" 14 "+t.I18nBot("tgbot.days")).WithCallbackData(t.encodeQuery("reset_exp_c "+email+" 14")), tu.InlineKeyboardButton(t.I18nBot("tgbot.add")+" 20 "+t.I18nBot("tgbot.days")).WithCallbackData(t.encodeQuery("reset_exp_c "+email+" 20")), ), tu.InlineKeyboardRow( tu.InlineKeyboardButton(t.I18nBot("tgbot.add")+" 1 "+t.I18nBot("tgbot.month")).WithCallbackData(t.encodeQuery("reset_exp_c "+email+" 30")), tu.InlineKeyboardButton(t.I18nBot("tgbot.add")+" 3 "+t.I18nBot("tgbot.months")).WithCallbackData(t.encodeQuery("reset_exp_c "+email+" 90")), ), tu.InlineKeyboardRow( tu.InlineKeyboardButton(t.I18nBot("tgbot.add")+" 6 "+t.I18nBot("tgbot.months")).WithCallbackData(t.encodeQuery("reset_exp_c "+email+" 180")), tu.InlineKeyboardButton(t.I18nBot("tgbot.add")+" 12 "+t.I18nBot("tgbot.months")).WithCallbackData(t.encodeQuery("reset_exp_c "+email+" 365")), ), ) t.editMessageCallbackTgBot(chatId, callbackQuery.Message.GetMessageID(), inlineKeyboard) case "reset_exp_c": if len(dataArray) == 3 { days, err := strconv.ParseInt(dataArray[2], 10, 64) if err == nil { var date int64 if days > 0 { traffic, err := t.inboundService.GetClientTrafficByEmail(email) if err != nil { logger.Warning(err) msg := t.I18nBot("tgbot.wentWrong") t.SendMsgToTgbot(chatId, msg) return } if traffic == nil { msg := t.I18nBot("tgbot.noResult") t.SendMsgToTgbot(chatId, msg) return } if traffic.ExpiryTime > 0 { if traffic.ExpiryTime-time.Now().Unix()*1000 < 0 { date = -int64(days * 24 * 60 * 60000) } else { date = traffic.ExpiryTime + int64(days*24*60*60000) } } else { date = traffic.ExpiryTime - int64(days*24*60*60000) } } needRestart, err := t.inboundService.ResetClientExpiryTimeByEmail(email, date) if needRestart { t.xrayService.SetToNeedRestart() } if err == nil { t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.expireResetSuccess", "Email=="+email)) t.searchClient(chatId, email, callbackQuery.Message.GetMessageID()) return } } } t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.errorOperation")) t.searchClient(chatId, email, callbackQuery.Message.GetMessageID()) case "reset_exp_in": if len(dataArray) >= 3 { oldInputNumber, err := strconv.Atoi(dataArray[2]) inputNumber := oldInputNumber if err == nil { if len(dataArray) == 4 { num, err := strconv.Atoi(dataArray[3]) if err == nil { switch num { case -2: inputNumber = 0 case -1: if inputNumber > 0 { inputNumber = (inputNumber / 10) } default: inputNumber = (inputNumber * 10) + num } } if inputNumber == oldInputNumber { t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.successfulOperation")) return } if inputNumber >= 999999 { t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.errorOperation")) return } } inlineKeyboard := tu.InlineKeyboard( tu.InlineKeyboardRow( tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.cancel")).WithCallbackData(t.encodeQuery("client_cancel "+email)), ), tu.InlineKeyboardRow( tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.confirmNumber", "Num=="+strconv.Itoa(inputNumber))).WithCallbackData(t.encodeQuery("reset_exp_c "+email+" "+strconv.Itoa(inputNumber))), ), tu.InlineKeyboardRow( tu.InlineKeyboardButton("1").WithCallbackData(t.encodeQuery("reset_exp_in "+email+" "+strconv.Itoa(inputNumber)+" 1")), tu.InlineKeyboardButton("2").WithCallbackData(t.encodeQuery("reset_exp_in "+email+" "+strconv.Itoa(inputNumber)+" 2")), tu.InlineKeyboardButton("3").WithCallbackData(t.encodeQuery("reset_exp_in "+email+" "+strconv.Itoa(inputNumber)+" 3")), ), tu.InlineKeyboardRow( tu.InlineKeyboardButton("4").WithCallbackData(t.encodeQuery("reset_exp_in "+email+" "+strconv.Itoa(inputNumber)+" 4")), tu.InlineKeyboardButton("5").WithCallbackData(t.encodeQuery("reset_exp_in "+email+" "+strconv.Itoa(inputNumber)+" 5")), tu.InlineKeyboardButton("6").WithCallbackData(t.encodeQuery("reset_exp_in "+email+" "+strconv.Itoa(inputNumber)+" 6")), ), tu.InlineKeyboardRow( tu.InlineKeyboardButton("7").WithCallbackData(t.encodeQuery("reset_exp_in "+email+" "+strconv.Itoa(inputNumber)+" 7")), tu.InlineKeyboardButton("8").WithCallbackData(t.encodeQuery("reset_exp_in "+email+" "+strconv.Itoa(inputNumber)+" 8")), tu.InlineKeyboardButton("9").WithCallbackData(t.encodeQuery("reset_exp_in "+email+" "+strconv.Itoa(inputNumber)+" 9")), ), tu.InlineKeyboardRow( tu.InlineKeyboardButton("🔄").WithCallbackData(t.encodeQuery("reset_exp_in "+email+" "+strconv.Itoa(inputNumber)+" -2")), tu.InlineKeyboardButton("0").WithCallbackData(t.encodeQuery("reset_exp_in "+email+" "+strconv.Itoa(inputNumber)+" 0")), tu.InlineKeyboardButton("⬅️").WithCallbackData(t.encodeQuery("reset_exp_in "+email+" "+strconv.Itoa(inputNumber)+" -1")), ), ) t.editMessageCallbackTgBot(chatId, callbackQuery.Message.GetMessageID(), inlineKeyboard) return } } t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.errorOperation")) t.searchClient(chatId, email, callbackQuery.Message.GetMessageID()) case "add_client_reset_exp_c": client_ExpiryTime = 0 days, _ := strconv.ParseInt(dataArray[1], 10, 64) var date int64 if client_ExpiryTime > 0 { if client_ExpiryTime-time.Now().Unix()*1000 < 0 { date = -int64(days * 24 * 60 * 60000) } else { date = client_ExpiryTime + int64(days*24*60*60000) } } else { date = client_ExpiryTime - int64(days*24*60*60000) } client_ExpiryTime = date messageId := callbackQuery.Message.GetMessageID() inbound, err := t.inboundService.GetInbound(receiver_inbound_ID) if err != nil { t.sendCallbackAnswerTgBot(callbackQuery.ID, err.Error()) return } message_text, err := t.BuildInboundClientDataMessage(inbound.Remark, inbound.Protocol) if err != nil { t.sendCallbackAnswerTgBot(callbackQuery.ID, err.Error()) return } t.addClient(callbackQuery.Message.GetChat().ID, message_text, messageId) t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.successfulOperation")) case "add_client_reset_exp_in": if len(dataArray) >= 2 { oldInputNumber, err := strconv.Atoi(dataArray[1]) inputNumber := oldInputNumber if err == nil { if len(dataArray) == 3 { num, err := strconv.Atoi(dataArray[2]) if err == nil { switch num { case -2: inputNumber = 0 case -1: if inputNumber > 0 { inputNumber = (inputNumber / 10) } default: inputNumber = (inputNumber * 10) + num } } if inputNumber == oldInputNumber { t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.successfulOperation")) return } if inputNumber >= 999999 { t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.errorOperation")) return } } inlineKeyboard := tu.InlineKeyboard( tu.InlineKeyboardRow( tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.cancel")).WithCallbackData(t.encodeQuery("add_client_default_traffic_exp")), ), tu.InlineKeyboardRow( tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.confirmNumberAdd", "Num=="+strconv.Itoa(inputNumber))).WithCallbackData(t.encodeQuery("add_client_reset_exp_c "+strconv.Itoa(inputNumber))), ), tu.InlineKeyboardRow( tu.InlineKeyboardButton("1").WithCallbackData(t.encodeQuery("add_client_reset_exp_in "+strconv.Itoa(inputNumber)+" 1")), tu.InlineKeyboardButton("2").WithCallbackData(t.encodeQuery("add_client_reset_exp_in "+strconv.Itoa(inputNumber)+" 2")), tu.InlineKeyboardButton("3").WithCallbackData(t.encodeQuery("add_client_reset_exp_in "+strconv.Itoa(inputNumber)+" 3")), ), tu.InlineKeyboardRow( tu.InlineKeyboardButton("4").WithCallbackData(t.encodeQuery("add_client_reset_exp_in "+strconv.Itoa(inputNumber)+" 4")), tu.InlineKeyboardButton("5").WithCallbackData(t.encodeQuery("add_client_reset_exp_in "+strconv.Itoa(inputNumber)+" 5")), tu.InlineKeyboardButton("6").WithCallbackData(t.encodeQuery("add_client_reset_exp_in "+strconv.Itoa(inputNumber)+" 6")), ), tu.InlineKeyboardRow( tu.InlineKeyboardButton("7").WithCallbackData(t.encodeQuery("add_client_reset_exp_in "+strconv.Itoa(inputNumber)+" 7")), tu.InlineKeyboardButton("8").WithCallbackData(t.encodeQuery("add_client_reset_exp_in "+strconv.Itoa(inputNumber)+" 8")), tu.InlineKeyboardButton("9").WithCallbackData(t.encodeQuery("add_client_reset_exp_in "+strconv.Itoa(inputNumber)+" 9")), ), tu.InlineKeyboardRow( tu.InlineKeyboardButton("🔄").WithCallbackData(t.encodeQuery("add_client_reset_exp_in "+strconv.Itoa(inputNumber)+" -2")), tu.InlineKeyboardButton("0").WithCallbackData(t.encodeQuery("add_client_reset_exp_in "+strconv.Itoa(inputNumber)+" 0")), tu.InlineKeyboardButton("⬅️").WithCallbackData(t.encodeQuery("add_client_reset_exp_in "+strconv.Itoa(inputNumber)+" -1")), ), ) t.editMessageCallbackTgBot(chatId, callbackQuery.Message.GetMessageID(), inlineKeyboard) return } } case "ip_limit": inlineKeyboard := tu.InlineKeyboard( tu.InlineKeyboardRow( tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.cancelIpLimit")).WithCallbackData(t.encodeQuery("client_cancel "+email)), ), tu.InlineKeyboardRow( tu.InlineKeyboardButton(t.I18nBot("tgbot.unlimited")).WithCallbackData(t.encodeQuery("ip_limit_c "+email+" 0")), tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.custom")).WithCallbackData(t.encodeQuery("ip_limit_in "+email+" 0")), ), tu.InlineKeyboardRow( tu.InlineKeyboardButton("1").WithCallbackData(t.encodeQuery("ip_limit_c "+email+" 1")), tu.InlineKeyboardButton("2").WithCallbackData(t.encodeQuery("ip_limit_c "+email+" 2")), ), tu.InlineKeyboardRow( tu.InlineKeyboardButton("3").WithCallbackData(t.encodeQuery("ip_limit_c "+email+" 3")), tu.InlineKeyboardButton("4").WithCallbackData(t.encodeQuery("ip_limit_c "+email+" 4")), ), tu.InlineKeyboardRow( tu.InlineKeyboardButton("5").WithCallbackData(t.encodeQuery("ip_limit_c "+email+" 5")), tu.InlineKeyboardButton("6").WithCallbackData(t.encodeQuery("ip_limit_c "+email+" 6")), tu.InlineKeyboardButton("7").WithCallbackData(t.encodeQuery("ip_limit_c "+email+" 7")), ), tu.InlineKeyboardRow( tu.InlineKeyboardButton("8").WithCallbackData(t.encodeQuery("ip_limit_c "+email+" 8")), tu.InlineKeyboardButton("9").WithCallbackData(t.encodeQuery("ip_limit_c "+email+" 9")), tu.InlineKeyboardButton("10").WithCallbackData(t.encodeQuery("ip_limit_c "+email+" 10")), ), ) t.editMessageCallbackTgBot(chatId, callbackQuery.Message.GetMessageID(), inlineKeyboard) case "ip_limit_c": if len(dataArray) == 3 { count, err := strconv.Atoi(dataArray[2]) if err == nil { needRestart, err := t.inboundService.ResetClientIpLimitByEmail(email, count) if needRestart { t.xrayService.SetToNeedRestart() } if err == nil { t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.resetIpSuccess", "Email=="+email, "Count=="+strconv.Itoa(count))) t.searchClient(chatId, email, callbackQuery.Message.GetMessageID()) return } } } t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.errorOperation")) t.searchClient(chatId, email, callbackQuery.Message.GetMessageID()) case "ip_limit_in": if len(dataArray) >= 3 { oldInputNumber, err := strconv.Atoi(dataArray[2]) inputNumber := oldInputNumber if err == nil { if len(dataArray) == 4 { num, err := strconv.Atoi(dataArray[3]) if err == nil { switch num { case -2: inputNumber = 0 case -1: if inputNumber > 0 { inputNumber = (inputNumber / 10) } default: inputNumber = (inputNumber * 10) + num } } if inputNumber == oldInputNumber { t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.successfulOperation")) return } if inputNumber >= 999999 { t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.errorOperation")) return } } inlineKeyboard := tu.InlineKeyboard( tu.InlineKeyboardRow( tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.cancel")).WithCallbackData(t.encodeQuery("client_cancel "+email)), ), tu.InlineKeyboardRow( tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.confirmNumber", "Num=="+strconv.Itoa(inputNumber))).WithCallbackData(t.encodeQuery("ip_limit_c "+email+" "+strconv.Itoa(inputNumber))), ), tu.InlineKeyboardRow( tu.InlineKeyboardButton("1").WithCallbackData(t.encodeQuery("ip_limit_in "+email+" "+strconv.Itoa(inputNumber)+" 1")), tu.InlineKeyboardButton("2").WithCallbackData(t.encodeQuery("ip_limit_in "+email+" "+strconv.Itoa(inputNumber)+" 2")), tu.InlineKeyboardButton("3").WithCallbackData(t.encodeQuery("ip_limit_in "+email+" "+strconv.Itoa(inputNumber)+" 3")), ), tu.InlineKeyboardRow( tu.InlineKeyboardButton("4").WithCallbackData(t.encodeQuery("ip_limit_in "+email+" "+strconv.Itoa(inputNumber)+" 4")), tu.InlineKeyboardButton("5").WithCallbackData(t.encodeQuery("ip_limit_in "+email+" "+strconv.Itoa(inputNumber)+" 5")), tu.InlineKeyboardButton("6").WithCallbackData(t.encodeQuery("ip_limit_in "+email+" "+strconv.Itoa(inputNumber)+" 6")), ), tu.InlineKeyboardRow( tu.InlineKeyboardButton("7").WithCallbackData(t.encodeQuery("ip_limit_in "+email+" "+strconv.Itoa(inputNumber)+" 7")), tu.InlineKeyboardButton("8").WithCallbackData(t.encodeQuery("ip_limit_in "+email+" "+strconv.Itoa(inputNumber)+" 8")), tu.InlineKeyboardButton("9").WithCallbackData(t.encodeQuery("ip_limit_in "+email+" "+strconv.Itoa(inputNumber)+" 9")), ), tu.InlineKeyboardRow( tu.InlineKeyboardButton("🔄").WithCallbackData(t.encodeQuery("ip_limit_in "+email+" "+strconv.Itoa(inputNumber)+" -2")), tu.InlineKeyboardButton("0").WithCallbackData(t.encodeQuery("ip_limit_in "+email+" "+strconv.Itoa(inputNumber)+" 0")), tu.InlineKeyboardButton("⬅️").WithCallbackData(t.encodeQuery("ip_limit_in "+email+" "+strconv.Itoa(inputNumber)+" -1")), ), ) t.editMessageCallbackTgBot(chatId, callbackQuery.Message.GetMessageID(), inlineKeyboard) return } } t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.errorOperation")) t.searchClient(chatId, email, callbackQuery.Message.GetMessageID()) case "add_client_ip_limit_c": if len(dataArray) == 2 { count, _ := strconv.Atoi(dataArray[1]) client_LimitIP = count } messageId := callbackQuery.Message.GetMessageID() inbound, err := t.inboundService.GetInbound(receiver_inbound_ID) if err != nil { t.sendCallbackAnswerTgBot(callbackQuery.ID, err.Error()) return } message_text, err := t.BuildInboundClientDataMessage(inbound.Remark, inbound.Protocol) if err != nil { t.sendCallbackAnswerTgBot(callbackQuery.ID, err.Error()) return } t.addClient(callbackQuery.Message.GetChat().ID, message_text, messageId) t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.successfulOperation")) case "add_client_ip_limit_in": if len(dataArray) >= 2 { oldInputNumber, err := strconv.Atoi(dataArray[1]) inputNumber := oldInputNumber if err == nil { if len(dataArray) == 3 { num, err := strconv.Atoi(dataArray[2]) if err == nil { switch num { case -2: inputNumber = 0 case -1: if inputNumber > 0 { inputNumber = (inputNumber / 10) } default: inputNumber = (inputNumber * 10) + num } } if inputNumber == oldInputNumber { t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.successfulOperation")) return } if inputNumber >= 999999 { t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.errorOperation")) return } } inlineKeyboard := tu.InlineKeyboard( tu.InlineKeyboardRow( tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.cancel")).WithCallbackData(t.encodeQuery("add_client_default_ip_limit")), ), tu.InlineKeyboardRow( tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.confirmNumber", "Num=="+strconv.Itoa(inputNumber))).WithCallbackData(t.encodeQuery("add_client_ip_limit_c "+strconv.Itoa(inputNumber))), ), tu.InlineKeyboardRow( tu.InlineKeyboardButton("1").WithCallbackData(t.encodeQuery("add_client_ip_limit_in "+strconv.Itoa(inputNumber)+" 1")), tu.InlineKeyboardButton("2").WithCallbackData(t.encodeQuery("add_client_ip_limit_in "+strconv.Itoa(inputNumber)+" 2")), tu.InlineKeyboardButton("3").WithCallbackData(t.encodeQuery("add_client_ip_limit_in "+strconv.Itoa(inputNumber)+" 3")), ), tu.InlineKeyboardRow( tu.InlineKeyboardButton("4").WithCallbackData(t.encodeQuery("add_client_ip_limit_in "+strconv.Itoa(inputNumber)+" 4")), tu.InlineKeyboardButton("5").WithCallbackData(t.encodeQuery("add_client_ip_limit_in "+strconv.Itoa(inputNumber)+" 5")), tu.InlineKeyboardButton("6").WithCallbackData(t.encodeQuery("add_client_ip_limit_in "+strconv.Itoa(inputNumber)+" 6")), ), tu.InlineKeyboardRow( tu.InlineKeyboardButton("7").WithCallbackData(t.encodeQuery("add_client_ip_limit_in "+strconv.Itoa(inputNumber)+" 7")), tu.InlineKeyboardButton("8").WithCallbackData(t.encodeQuery("add_client_ip_limit_in "+strconv.Itoa(inputNumber)+" 8")), tu.InlineKeyboardButton("9").WithCallbackData(t.encodeQuery("add_client_ip_limit_in "+strconv.Itoa(inputNumber)+" 9")), ), tu.InlineKeyboardRow( tu.InlineKeyboardButton("🔄").WithCallbackData(t.encodeQuery("add_client_ip_limit_in "+strconv.Itoa(inputNumber)+" -2")), tu.InlineKeyboardButton("0").WithCallbackData(t.encodeQuery("add_client_ip_limit_in "+strconv.Itoa(inputNumber)+" 0")), tu.InlineKeyboardButton("⬅️").WithCallbackData(t.encodeQuery("add_client_ip_limit_in "+strconv.Itoa(inputNumber)+" -1")), ), ) t.editMessageCallbackTgBot(chatId, callbackQuery.Message.GetMessageID(), inlineKeyboard) return } } case "clear_ips": inlineKeyboard := tu.InlineKeyboard( tu.InlineKeyboardRow( tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.cancel")).WithCallbackData(t.encodeQuery("ips_cancel "+email)), ), tu.InlineKeyboardRow( tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.confirmClearIps")).WithCallbackData(t.encodeQuery("clear_ips_c "+email)), ), ) t.editMessageCallbackTgBot(chatId, callbackQuery.Message.GetMessageID(), inlineKeyboard) case "clear_ips_c": err := t.inboundService.ClearClientIps(email) if err == nil { t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.clearIpSuccess", "Email=="+email)) t.searchClientIps(chatId, email, callbackQuery.Message.GetMessageID()) } else { t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.errorOperation")) } case "ip_log": t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.getIpLog", "Email=="+email)) t.searchClientIps(chatId, email) case "tg_user": t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.getUserInfo", "Email=="+email)) t.clientTelegramUserInfo(chatId, email) case "tgid_remove": inlineKeyboard := tu.InlineKeyboard( tu.InlineKeyboardRow( tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.cancel")).WithCallbackData(t.encodeQuery("tgid_cancel "+email)), ), tu.InlineKeyboardRow( tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.confirmRemoveTGUser")).WithCallbackData(t.encodeQuery("tgid_remove_c "+email)), ), ) t.editMessageCallbackTgBot(chatId, callbackQuery.Message.GetMessageID(), inlineKeyboard) case "tgid_remove_c": traffic, err := t.inboundService.GetClientTrafficByEmail(email) if err != nil || traffic == nil { t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.errorOperation")) return } needRestart, err := t.inboundService.SetClientTelegramUserID(traffic.Id, EmptyTelegramUserID) if needRestart { t.xrayService.SetToNeedRestart() } if err == nil { t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.removedTGUserSuccess", "Email=="+email)) t.clientTelegramUserInfo(chatId, email, callbackQuery.Message.GetMessageID()) } else { t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.errorOperation")) } case "toggle_enable": inlineKeyboard := tu.InlineKeyboard( tu.InlineKeyboardRow( tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.cancel")).WithCallbackData(t.encodeQuery("client_cancel "+email)), ), tu.InlineKeyboardRow( tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.confirmToggle")).WithCallbackData(t.encodeQuery("toggle_enable_c "+email)), ), ) t.editMessageCallbackTgBot(chatId, callbackQuery.Message.GetMessageID(), inlineKeyboard) case "toggle_enable_c": enabled, needRestart, err := t.inboundService.ToggleClientEnableByEmail(email) if needRestart { t.xrayService.SetToNeedRestart() } if err == nil { if enabled { t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.enableSuccess", "Email=="+email)) } else { t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.disableSuccess", "Email=="+email)) } t.searchClient(chatId, email, callbackQuery.Message.GetMessageID()) } else { t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.errorOperation")) } case "get_clients": inboundId := dataArray[1] inboundIdInt, err := strconv.Atoi(inboundId) if err != nil { t.sendCallbackAnswerTgBot(callbackQuery.ID, err.Error()) return } inbound, err := t.inboundService.GetInbound(inboundIdInt) if err != nil { t.sendCallbackAnswerTgBot(callbackQuery.ID, err.Error()) return } clients, err := t.getInboundClients(inboundIdInt) if err != nil { t.sendCallbackAnswerTgBot(callbackQuery.ID, err.Error()) return } t.SendMsgToTgbot(chatId, t.I18nBot("tgbot.answers.chooseClient", "Inbound=="+inbound.Remark), clients) case "add_client_to": // assign default values to clients variables client_Id = uuid.New().String() client_Flow = "" client_Email = t.randomLowerAndNum(8) client_LimitIP = 0 client_TotalGB = 0 client_ExpiryTime = 0 client_Enable = true client_TgID = "" client_SubID = t.randomLowerAndNum(16) client_Comment = "" client_Reset = 0 client_Security = "auto" client_ShPassword = t.randomShadowSocksPassword() client_TrPassword = t.randomLowerAndNum(10) client_Method = "" inboundId := dataArray[1] inboundIdInt, err := strconv.Atoi(inboundId) if err != nil { t.sendCallbackAnswerTgBot(callbackQuery.ID, err.Error()) return } receiver_inbound_ID = inboundIdInt inbound, err := t.inboundService.GetInbound(inboundIdInt) if err != nil { t.sendCallbackAnswerTgBot(callbackQuery.ID, err.Error()) return } message_text, err := t.BuildInboundClientDataMessage(inbound.Remark, inbound.Protocol) if err != nil { t.sendCallbackAnswerTgBot(callbackQuery.ID, err.Error()) return } t.addClient(callbackQuery.Message.GetChat().ID, message_text) } return } else { switch callbackQuery.Data { case "get_inbounds": inbounds, err := t.getInbounds() if err != nil { t.sendCallbackAnswerTgBot(callbackQuery.ID, err.Error()) return } t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.buttons.allClients")) t.SendMsgToTgbot(chatId, t.I18nBot("tgbot.answers.chooseInbound"), inbounds) case "admin_client_sub_links": inbounds, err := t.getInboundsFor("get_clients_for_sub") if err != nil { t.sendCallbackAnswerTgBot(callbackQuery.ID, err.Error()) return } t.SendMsgToTgbot(chatId, t.I18nBot("tgbot.answers.chooseInbound"), inbounds) case "admin_client_individual_links": inbounds, err := t.getInboundsFor("get_clients_for_individual") if err != nil { t.sendCallbackAnswerTgBot(callbackQuery.ID, err.Error()) return } t.SendMsgToTgbot(chatId, t.I18nBot("tgbot.answers.chooseInbound"), inbounds) case "admin_client_qr_links": inbounds, err := t.getInboundsFor("get_clients_for_qr") if err != nil { t.sendCallbackAnswerTgBot(callbackQuery.ID, err.Error()) return } t.SendMsgToTgbot(chatId, t.I18nBot("tgbot.answers.chooseInbound"), inbounds) } } } switch callbackQuery.Data { case "get_usage": t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.buttons.serverUsage")) t.getServerUsage(chatId) case "usage_refresh": t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.successfulOperation")) t.getServerUsage(chatId, callbackQuery.Message.GetMessageID()) case "inbounds": t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.buttons.getInbounds")) t.SendMsgToTgbot(chatId, t.getInboundUsages()) case "deplete_soon": t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.buttons.depleteSoon")) t.getExhausted(chatId) case "get_backup": t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.buttons.dbBackup")) t.sendBackup(chatId) case "get_banlogs": t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.buttons.getBanLogs")) t.sendBanLogs(chatId, true) case "client_traffic": tgUserID := callbackQuery.From.ID t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.buttons.clientUsage")) t.getClientUsage(chatId, tgUserID) case "client_commands": t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.buttons.commands")) t.SendMsgToTgbot(chatId, t.I18nBot("tgbot.commands.helpClientCommands")) case "client_sub_links": // show user's own clients to choose one for sub links tgUserID := callbackQuery.From.ID traffics, err := t.inboundService.GetClientTrafficTgBot(tgUserID) if err != nil { // fallback to message t.SendMsgToTgbot(chatId, t.I18nBot("tgbot.answers.errorOperation")+"\r\n"+err.Error()) return } if len(traffics) == 0 { t.SendMsgToTgbot(chatId, t.I18nBot("tgbot.answers.askToAddUserId", "TgUserID=="+strconv.FormatInt(tgUserID, 10))) return } var buttons []telego.InlineKeyboardButton for _, tr := range traffics { buttons = append(buttons, tu.InlineKeyboardButton(tr.Email).WithCallbackData(t.encodeQuery("client_sub_links "+tr.Email))) } cols := 1 if len(buttons) >= 6 { cols = 2 } keyboard := tu.InlineKeyboardGrid(tu.InlineKeyboardCols(cols, buttons...)) t.SendMsgToTgbot(chatId, t.I18nBot("tgbot.commands.pleaseChoose"), keyboard) case "client_individual_links": // show user's clients to choose for individual links tgUserID := callbackQuery.From.ID traffics, err := t.inboundService.GetClientTrafficTgBot(tgUserID) if err != nil { t.SendMsgToTgbot(chatId, t.I18nBot("tgbot.answers.errorOperation")+"\r\n"+err.Error()) return } if len(traffics) == 0 { t.SendMsgToTgbot(chatId, t.I18nBot("tgbot.answers.askToAddUserId", "TgUserID=="+strconv.FormatInt(tgUserID, 10))) return } var buttons2 []telego.InlineKeyboardButton for _, tr := range traffics { buttons2 = append(buttons2, tu.InlineKeyboardButton(tr.Email).WithCallbackData(t.encodeQuery("client_individual_links "+tr.Email))) } cols2 := 1 if len(buttons2) >= 6 { cols2 = 2 } keyboard2 := tu.InlineKeyboardGrid(tu.InlineKeyboardCols(cols2, buttons2...)) t.SendMsgToTgbot(chatId, t.I18nBot("tgbot.commands.pleaseChoose"), keyboard2) case "client_qr_links": // show user's clients to choose for QR codes tgUserID := callbackQuery.From.ID traffics, err := t.inboundService.GetClientTrafficTgBot(tgUserID) if err != nil { t.SendMsgToTgbot(chatId, t.I18nBot("tgbot.answers.errorOccurred")+"\r\n"+err.Error()) return } if len(traffics) == 0 { t.SendMsgToTgbot(chatId, t.I18nBot("tgbot.answers.askToAddUserId", "TgUserID=="+strconv.FormatInt(tgUserID, 10))) return } var buttons3 []telego.InlineKeyboardButton for _, tr := range traffics { buttons3 = append(buttons3, tu.InlineKeyboardButton(tr.Email).WithCallbackData(t.encodeQuery("client_qr_links "+tr.Email))) } cols3 := 1 if len(buttons3) >= 6 { cols3 = 2 } keyboard3 := tu.InlineKeyboardGrid(tu.InlineKeyboardCols(cols3, buttons3...)) t.SendMsgToTgbot(chatId, t.I18nBot("tgbot.commands.pleaseChoose"), keyboard3) case "onlines": t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.buttons.onlines")) t.onlineClients(chatId) case "onlines_refresh": t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.successfulOperation")) t.onlineClients(chatId, callbackQuery.Message.GetMessageID()) case "commands": t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.buttons.commands")) t.SendMsgToTgbot(chatId, t.I18nBot("tgbot.commands.helpAdminCommands")) case "add_client": // assign default values to clients variables client_Id = uuid.New().String() client_Flow = "" client_Email = t.randomLowerAndNum(8) client_LimitIP = 0 client_TotalGB = 0 client_ExpiryTime = 0 client_Enable = true client_TgID = "" client_SubID = t.randomLowerAndNum(16) client_Comment = "" client_Reset = 0 client_Security = "auto" client_ShPassword = t.randomShadowSocksPassword() client_TrPassword = t.randomLowerAndNum(10) client_Method = "" inbounds, err := t.getInboundsAddClient() if err != nil { t.sendCallbackAnswerTgBot(callbackQuery.ID, err.Error()) return } t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.buttons.addClient")) t.SendMsgToTgbot(chatId, t.I18nBot("tgbot.answers.chooseInbound"), inbounds) case "add_client_ch_default_email": t.deleteMessageTgBot(chatId, callbackQuery.Message.GetMessageID()) userStates[chatId] = "awaiting_email" cancel_btn_markup := tu.InlineKeyboard( tu.InlineKeyboardRow( tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.use_default")).WithCallbackData("add_client_default_info"), ), ) prompt_message := t.I18nBot("tgbot.messages.email_prompt", "ClientEmail=="+client_Email) t.SendMsgToTgbot(chatId, prompt_message, cancel_btn_markup) case "add_client_ch_default_id": t.deleteMessageTgBot(chatId, callbackQuery.Message.GetMessageID()) userStates[chatId] = "awaiting_id" cancel_btn_markup := tu.InlineKeyboard( tu.InlineKeyboardRow( tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.use_default")).WithCallbackData("add_client_default_info"), ), ) prompt_message := t.I18nBot("tgbot.messages.id_prompt", "ClientId=="+client_Id) t.SendMsgToTgbot(chatId, prompt_message, cancel_btn_markup) case "add_client_ch_default_pass_tr": t.deleteMessageTgBot(chatId, callbackQuery.Message.GetMessageID()) userStates[chatId] = "awaiting_password_tr" cancel_btn_markup := tu.InlineKeyboard( tu.InlineKeyboardRow( tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.use_default")).WithCallbackData("add_client_default_info"), ), ) prompt_message := t.I18nBot("tgbot.messages.pass_prompt", "ClientPassword=="+client_TrPassword) t.SendMsgToTgbot(chatId, prompt_message, cancel_btn_markup) case "add_client_ch_default_pass_sh": t.deleteMessageTgBot(chatId, callbackQuery.Message.GetMessageID()) userStates[chatId] = "awaiting_password_sh" cancel_btn_markup := tu.InlineKeyboard( tu.InlineKeyboardRow( tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.use_default")).WithCallbackData("add_client_default_info"), ), ) prompt_message := t.I18nBot("tgbot.messages.pass_prompt", "ClientPassword=="+client_ShPassword) t.SendMsgToTgbot(chatId, prompt_message, cancel_btn_markup) case "add_client_ch_default_comment": t.deleteMessageTgBot(chatId, callbackQuery.Message.GetMessageID()) userStates[chatId] = "awaiting_comment" cancel_btn_markup := tu.InlineKeyboard( tu.InlineKeyboardRow( tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.use_default")).WithCallbackData("add_client_default_info"), ), ) prompt_message := t.I18nBot("tgbot.messages.comment_prompt", "ClientComment=="+client_Comment) t.SendMsgToTgbot(chatId, prompt_message, cancel_btn_markup) case "add_client_ch_default_traffic": inlineKeyboard := tu.InlineKeyboard( tu.InlineKeyboardRow( tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.cancel")).WithCallbackData(t.encodeQuery("add_client_default_traffic_exp")), ), tu.InlineKeyboardRow( tu.InlineKeyboardButton(t.I18nBot("tgbot.unlimited")).WithCallbackData(t.encodeQuery("add_client_limit_traffic_c 0")), tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.custom")).WithCallbackData(t.encodeQuery("add_client_limit_traffic_in 0")), ), tu.InlineKeyboardRow( tu.InlineKeyboardButton("1 GB").WithCallbackData(t.encodeQuery("add_client_limit_traffic_c 1")), tu.InlineKeyboardButton("5 GB").WithCallbackData(t.encodeQuery("add_client_limit_traffic_c 5")), tu.InlineKeyboardButton("10 GB").WithCallbackData(t.encodeQuery("add_client_limit_traffic_c 10")), ), tu.InlineKeyboardRow( tu.InlineKeyboardButton("20 GB").WithCallbackData(t.encodeQuery("add_client_limit_traffic_c 20")), tu.InlineKeyboardButton("30 GB").WithCallbackData(t.encodeQuery("add_client_limit_traffic_c 30")), tu.InlineKeyboardButton("40 GB").WithCallbackData(t.encodeQuery("add_client_limit_traffic_c 40")), ), tu.InlineKeyboardRow( tu.InlineKeyboardButton("50 GB").WithCallbackData(t.encodeQuery("add_client_limit_traffic_c 50")), tu.InlineKeyboardButton("60 GB").WithCallbackData(t.encodeQuery("add_client_limit_traffic_c 60")), tu.InlineKeyboardButton("80 GB").WithCallbackData(t.encodeQuery("add_client_limit_traffic_c 80")), ), tu.InlineKeyboardRow( tu.InlineKeyboardButton("100 GB").WithCallbackData(t.encodeQuery("add_client_limit_traffic_c 100")), tu.InlineKeyboardButton("150 GB").WithCallbackData(t.encodeQuery("add_client_limit_traffic_c 150")), tu.InlineKeyboardButton("200 GB").WithCallbackData(t.encodeQuery("add_client_limit_traffic_c 200")), ), ) t.editMessageCallbackTgBot(chatId, callbackQuery.Message.GetMessageID(), inlineKeyboard) case "add_client_ch_default_exp": inlineKeyboard := tu.InlineKeyboard( tu.InlineKeyboardRow( tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.cancel")).WithCallbackData(t.encodeQuery("add_client_default_traffic_exp")), ), tu.InlineKeyboardRow( tu.InlineKeyboardButton(t.I18nBot("tgbot.unlimited")).WithCallbackData(t.encodeQuery("add_client_reset_exp_c 0")), tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.custom")).WithCallbackData(t.encodeQuery("add_client_reset_exp_in 0")), ), tu.InlineKeyboardRow( tu.InlineKeyboardButton(t.I18nBot("tgbot.add")+" 7 "+t.I18nBot("tgbot.days")).WithCallbackData(t.encodeQuery("add_client_reset_exp_c 7")), tu.InlineKeyboardButton(t.I18nBot("tgbot.add")+" 10 "+t.I18nBot("tgbot.days")).WithCallbackData(t.encodeQuery("add_client_reset_exp_c 10")), ), tu.InlineKeyboardRow( tu.InlineKeyboardButton(t.I18nBot("tgbot.add")+" 14 "+t.I18nBot("tgbot.days")).WithCallbackData(t.encodeQuery("add_client_reset_exp_c 14")), tu.InlineKeyboardButton(t.I18nBot("tgbot.add")+" 20 "+t.I18nBot("tgbot.days")).WithCallbackData(t.encodeQuery("add_client_reset_exp_c 20")), ), tu.InlineKeyboardRow( tu.InlineKeyboardButton(t.I18nBot("tgbot.add")+" 1 "+t.I18nBot("tgbot.month")).WithCallbackData(t.encodeQuery("add_client_reset_exp_c 30")), tu.InlineKeyboardButton(t.I18nBot("tgbot.add")+" 3 "+t.I18nBot("tgbot.months")).WithCallbackData(t.encodeQuery("add_client_reset_exp_c 90")), ), tu.InlineKeyboardRow( tu.InlineKeyboardButton(t.I18nBot("tgbot.add")+" 6 "+t.I18nBot("tgbot.months")).WithCallbackData(t.encodeQuery("add_client_reset_exp_c 180")), tu.InlineKeyboardButton(t.I18nBot("tgbot.add")+" 12 "+t.I18nBot("tgbot.months")).WithCallbackData(t.encodeQuery("add_client_reset_exp_c 365")), ), ) t.editMessageCallbackTgBot(chatId, callbackQuery.Message.GetMessageID(), inlineKeyboard) case "add_client_ch_default_ip_limit": inlineKeyboard := tu.InlineKeyboard( tu.InlineKeyboardRow( tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.cancel")).WithCallbackData(t.encodeQuery("add_client_default_ip_limit")), ), tu.InlineKeyboardRow( tu.InlineKeyboardButton(t.I18nBot("tgbot.unlimited")).WithCallbackData(t.encodeQuery("add_client_ip_limit_c 0")), tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.custom")).WithCallbackData(t.encodeQuery("add_client_ip_limit_in 0")), ), tu.InlineKeyboardRow( tu.InlineKeyboardButton("1").WithCallbackData(t.encodeQuery("add_client_ip_limit_c 1")), tu.InlineKeyboardButton("2").WithCallbackData(t.encodeQuery("add_client_ip_limit_c 2")), ), tu.InlineKeyboardRow( tu.InlineKeyboardButton("3").WithCallbackData(t.encodeQuery("add_client_ip_limit_c 3")), tu.InlineKeyboardButton("4").WithCallbackData(t.encodeQuery("add_client_ip_limit_c 4")), ), tu.InlineKeyboardRow( tu.InlineKeyboardButton("5").WithCallbackData(t.encodeQuery("add_client_ip_limit_c 5")), tu.InlineKeyboardButton("6").WithCallbackData(t.encodeQuery("add_client_ip_limit_c 6")), tu.InlineKeyboardButton("7").WithCallbackData(t.encodeQuery("add_client_ip_limit_c 7")), ), tu.InlineKeyboardRow( tu.InlineKeyboardButton("8").WithCallbackData(t.encodeQuery("add_client_ip_limit_c 8")), tu.InlineKeyboardButton("9").WithCallbackData(t.encodeQuery("add_client_ip_limit_c 9")), tu.InlineKeyboardButton("10").WithCallbackData(t.encodeQuery("add_client_ip_limit_c 10")), ), ) t.editMessageCallbackTgBot(chatId, callbackQuery.Message.GetMessageID(), inlineKeyboard) case "add_client_default_info": t.deleteMessageTgBot(chatId, callbackQuery.Message.GetMessageID()) t.SendMsgToTgbotDeleteAfter(chatId, t.I18nBot("tgbot.messages.using_default_value"), 3, tu.ReplyKeyboardRemove()) delete(userStates, chatId) inbound, _ := t.inboundService.GetInbound(receiver_inbound_ID) message_text, _ := t.BuildInboundClientDataMessage(inbound.Remark, inbound.Protocol) t.addClient(chatId, message_text) case "add_client_cancel": delete(userStates, chatId) t.deleteMessageTgBot(chatId, callbackQuery.Message.GetMessageID()) t.SendMsgToTgbotDeleteAfter(chatId, t.I18nBot("tgbot.messages.cancel"), 3, tu.ReplyKeyboardRemove()) case "add_client_default_traffic_exp": messageId := callbackQuery.Message.GetMessageID() inbound, err := t.inboundService.GetInbound(receiver_inbound_ID) if err != nil { t.sendCallbackAnswerTgBot(callbackQuery.ID, err.Error()) return } message_text, err := t.BuildInboundClientDataMessage(inbound.Remark, inbound.Protocol) if err != nil { t.sendCallbackAnswerTgBot(callbackQuery.ID, err.Error()) return } t.addClient(chatId, message_text, messageId) t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.canceled", "Email=="+client_Email)) case "add_client_default_ip_limit": messageId := callbackQuery.Message.GetMessageID() inbound, err := t.inboundService.GetInbound(receiver_inbound_ID) if err != nil { t.sendCallbackAnswerTgBot(callbackQuery.ID, err.Error()) return } message_text, err := t.BuildInboundClientDataMessage(inbound.Remark, inbound.Protocol) if err != nil { t.sendCallbackAnswerTgBot(callbackQuery.ID, err.Error()) return } t.addClient(chatId, message_text, messageId) t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.canceled", "Email=="+client_Email)) case "add_client_submit_disable": client_Enable = false _, err := t.SubmitAddClient() if err != nil { errorMessage := fmt.Sprintf("%v", err) t.SendMsgToTgbot(chatId, t.I18nBot("tgbot.messages.error_add_client", "error=="+errorMessage), tu.ReplyKeyboardRemove()) } else { t.deleteMessageTgBot(chatId, callbackQuery.Message.GetMessageID()) t.SendMsgToTgbot(chatId, t.I18nBot("tgbot.answers.successfulOperation"), tu.ReplyKeyboardRemove()) t.sendClientIndividualLinks(chatId, client_Email) t.sendClientQRLinks(chatId, client_Email) } case "add_client_submit_enable": client_Enable = true _, err := t.SubmitAddClient() if err != nil { errorMessage := fmt.Sprintf("%v", err) t.SendMsgToTgbot(chatId, t.I18nBot("tgbot.messages.error_add_client", "error=="+errorMessage), tu.ReplyKeyboardRemove()) } else { t.deleteMessageTgBot(chatId, callbackQuery.Message.GetMessageID()) t.SendMsgToTgbot(chatId, t.I18nBot("tgbot.answers.successfulOperation"), tu.ReplyKeyboardRemove()) t.sendClientIndividualLinks(chatId, client_Email) t.sendClientQRLinks(chatId, client_Email) } case "reset_all_traffics_cancel": t.deleteMessageTgBot(chatId, callbackQuery.Message.GetMessageID()) t.SendMsgToTgbotDeleteAfter(chatId, t.I18nBot("tgbot.messages.cancel"), 1, tu.ReplyKeyboardRemove()) case "reset_all_traffics": inlineKeyboard := tu.InlineKeyboard( tu.InlineKeyboardRow( tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.cancelReset")).WithCallbackData(t.encodeQuery("reset_all_traffics_cancel")), ), tu.InlineKeyboardRow( tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.confirmResetTraffic")).WithCallbackData(t.encodeQuery("reset_all_traffics_c")), ), ) t.SendMsgToTgbot(chatId, t.I18nBot("tgbot.messages.AreYouSure"), inlineKeyboard) case "reset_all_traffics_c": t.deleteMessageTgBot(chatId, callbackQuery.Message.GetMessageID()) emails, err := t.inboundService.getAllEmails() if err != nil { t.SendMsgToTgbot(chatId, t.I18nBot("tgbot.answers.errorOperation"), tu.ReplyKeyboardRemove()) return } for _, email := range emails { err := t.inboundService.ResetClientTrafficByEmail(email) if err == nil { msg := t.I18nBot("tgbot.messages.SuccessResetTraffic", "ClientEmail=="+email) t.SendMsgToTgbot(chatId, msg, tu.ReplyKeyboardRemove()) } else { msg := t.I18nBot("tgbot.messages.FailedResetTraffic", "ClientEmail=="+email, "ErrorMessage=="+err.Error()) t.SendMsgToTgbot(chatId, msg, tu.ReplyKeyboardRemove()) } } t.SendMsgToTgbot(chatId, t.I18nBot("tgbot.messages.FinishProcess"), tu.ReplyKeyboardRemove()) case "get_sorted_traffic_usage_report": t.deleteMessageTgBot(chatId, callbackQuery.Message.GetMessageID()) emails, err := t.inboundService.getAllEmails() if err != nil { t.SendMsgToTgbot(chatId, t.I18nBot("tgbot.answers.errorOperation"), tu.ReplyKeyboardRemove()) return } valid_emails, extra_emails, err := t.inboundService.FilterAndSortClientEmails(emails) if err != nil { t.SendMsgToTgbot(chatId, t.I18nBot("tgbot.answers.errorOperation"), tu.ReplyKeyboardRemove()) return } for _, valid_emails := range valid_emails { traffic, err := t.inboundService.GetClientTrafficByEmail(valid_emails) if err != nil { logger.Warning(err) msg := t.I18nBot("tgbot.wentWrong") t.SendMsgToTgbot(chatId, msg) continue } if traffic == nil { msg := t.I18nBot("tgbot.noResult") t.SendMsgToTgbot(chatId, msg) continue } output := t.clientInfoMsg(traffic, false, false, false, false, true, false) t.SendMsgToTgbot(chatId, output, tu.ReplyKeyboardRemove()) } for _, extra_emails := range extra_emails { msg := fmt.Sprintf("📧 %s\n%s", extra_emails, t.I18nBot("tgbot.noResult")) t.SendMsgToTgbot(chatId, msg, tu.ReplyKeyboardRemove()) } default: if after, ok := strings.CutPrefix(callbackQuery.Data, "client_sub_links "); ok { email := after t.sendClientSubLinks(chatId, email) return } if after, ok := strings.CutPrefix(callbackQuery.Data, "client_individual_links "); ok { email := after t.sendClientIndividualLinks(chatId, email) return } if after, ok := strings.CutPrefix(callbackQuery.Data, "client_qr_links "); ok { email := after t.sendClientQRLinks(chatId, email) return } } } // BuildInboundClientDataMessage builds a message with client data for the given inbound and protocol. func (t *Tgbot) BuildInboundClientDataMessage(inbound_remark string, protocol model.Protocol) (string, error) { var message string currentTime := time.Now() timestampMillis := currentTime.UnixNano() / int64(time.Millisecond) expiryTime := "" diff := client_ExpiryTime/1000 - timestampMillis if client_ExpiryTime == 0 { expiryTime = t.I18nBot("tgbot.unlimited") } else if diff > 172800 { expiryTime = time.Unix((client_ExpiryTime / 1000), 0).Format("2006-01-02 15:04:05") } else if client_ExpiryTime < 0 { expiryTime = fmt.Sprintf("%d %s", client_ExpiryTime/-86400000, t.I18nBot("tgbot.days")) } else { expiryTime = fmt.Sprintf("%d %s", diff/3600, t.I18nBot("tgbot.hours")) } traffic_value := "" if client_TotalGB == 0 { traffic_value = "♾️ Unlimited(Reset)" } else { traffic_value = common.FormatTraffic(client_TotalGB) } ip_limit := "" if client_LimitIP == 0 { ip_limit = "♾️ Unlimited(Reset)" } else { ip_limit = fmt.Sprint(client_LimitIP) } switch protocol { case model.VMESS, model.VLESS: message = t.I18nBot("tgbot.messages.inbound_client_data_id", "InboundRemark=="+inbound_remark, "ClientId=="+client_Id, "ClientEmail=="+client_Email, "ClientTraffic=="+traffic_value, "ClientExp=="+expiryTime, "IpLimit=="+ip_limit, "ClientComment=="+client_Comment) case model.Trojan: message = t.I18nBot("tgbot.messages.inbound_client_data_pass", "InboundRemark=="+inbound_remark, "ClientPass=="+client_TrPassword, "ClientEmail=="+client_Email, "ClientTraffic=="+traffic_value, "ClientExp=="+expiryTime, "IpLimit=="+ip_limit, "ClientComment=="+client_Comment) case model.Shadowsocks: message = t.I18nBot("tgbot.messages.inbound_client_data_pass", "InboundRemark=="+inbound_remark, "ClientPass=="+client_ShPassword, "ClientEmail=="+client_Email, "ClientTraffic=="+traffic_value, "ClientExp=="+expiryTime, "IpLimit=="+ip_limit, "ClientComment=="+client_Comment) default: return "", errors.New("unknown protocol") } return message, nil } // BuildJSONForProtocol builds a JSON string for the given protocol with client data. func (t *Tgbot) BuildJSONForProtocol(protocol model.Protocol) (string, error) { var jsonString string switch protocol { case model.VMESS: jsonString = fmt.Sprintf(`{ "clients": [{ "id": "%s", "security": "%s", "email": "%s", "limitIp": %d, "totalGB": %d, "expiryTime": %d, "enable": %t, "tgId": "%s", "subId": "%s", "comment": "%s", "reset": %d }] }`, client_Id, client_Security, client_Email, client_LimitIP, client_TotalGB, client_ExpiryTime, client_Enable, client_TgID, client_SubID, client_Comment, client_Reset) case model.VLESS: jsonString = fmt.Sprintf(`{ "clients": [{ "id": "%s", "flow": "%s", "email": "%s", "limitIp": %d, "totalGB": %d, "expiryTime": %d, "enable": %t, "tgId": "%s", "subId": "%s", "comment": "%s", "reset": %d }] }`, client_Id, client_Flow, client_Email, client_LimitIP, client_TotalGB, client_ExpiryTime, client_Enable, client_TgID, client_SubID, client_Comment, client_Reset) case model.Trojan: jsonString = fmt.Sprintf(`{ "clients": [{ "password": "%s", "email": "%s", "limitIp": %d, "totalGB": %d, "expiryTime": %d, "enable": %t, "tgId": "%s", "subId": "%s", "comment": "%s", "reset": %d }] }`, client_TrPassword, client_Email, client_LimitIP, client_TotalGB, client_ExpiryTime, client_Enable, client_TgID, client_SubID, client_Comment, client_Reset) case model.Shadowsocks: jsonString = fmt.Sprintf(`{ "clients": [{ "method": "%s", "password": "%s", "email": "%s", "limitIp": %d, "totalGB": %d, "expiryTime": %d, "enable": %t, "tgId": "%s", "subId": "%s", "comment": "%s", "reset": %d }] }`, client_Method, client_ShPassword, client_Email, client_LimitIP, client_TotalGB, client_ExpiryTime, client_Enable, client_TgID, client_SubID, client_Comment, client_Reset) default: return "", errors.New("unknown protocol") } return jsonString, nil } // SubmitAddClient submits the client addition request to the inbound service. func (t *Tgbot) SubmitAddClient() (bool, error) { inbound, err := t.inboundService.GetInbound(receiver_inbound_ID) if err != nil { logger.Warning("getIboundClients run failed:", err) return false, errors.New(t.I18nBot("tgbot.answers.getInboundsFailed")) } jsonString, err := t.BuildJSONForProtocol(inbound.Protocol) if err != nil { logger.Warning("BuildJSONForProtocol run failed:", err) return false, errors.New("failed to build JSON for protocol") } newInbound := &model.Inbound{ Id: receiver_inbound_ID, Settings: jsonString, } return t.inboundService.AddInboundClient(newInbound) } // checkAdmin checks if the given Telegram ID is an admin. func checkAdmin(tgId int64) bool { for _, adminId := range adminIds { if adminId == tgId { return true } } return false } // SendAnswer sends a response message with an inline keyboard to the specified chat. func (t *Tgbot) SendAnswer(chatId int64, msg string, isAdmin bool) { numericKeyboard := tu.InlineKeyboard( tu.InlineKeyboardRow( tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.SortedTrafficUsageReport")).WithCallbackData(t.encodeQuery("get_sorted_traffic_usage_report")), ), tu.InlineKeyboardRow( tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.serverUsage")).WithCallbackData(t.encodeQuery("get_usage")), tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.ResetAllTraffics")).WithCallbackData(t.encodeQuery("reset_all_traffics")), ), tu.InlineKeyboardRow( tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.dbBackup")).WithCallbackData(t.encodeQuery("get_backup")), tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.getBanLogs")).WithCallbackData(t.encodeQuery("get_banlogs")), ), tu.InlineKeyboardRow( tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.getInbounds")).WithCallbackData(t.encodeQuery("inbounds")), tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.depleteSoon")).WithCallbackData(t.encodeQuery("deplete_soon")), ), tu.InlineKeyboardRow( tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.commands")).WithCallbackData(t.encodeQuery("commands")), tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.onlines")).WithCallbackData(t.encodeQuery("onlines")), ), tu.InlineKeyboardRow( tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.allClients")).WithCallbackData(t.encodeQuery("get_inbounds")), tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.addClient")).WithCallbackData(t.encodeQuery("add_client")), ), tu.InlineKeyboardRow( tu.InlineKeyboardButton(t.I18nBot("pages.settings.subSettings")).WithCallbackData(t.encodeQuery("admin_client_sub_links")), tu.InlineKeyboardButton(t.I18nBot("subscription.individualLinks")).WithCallbackData(t.encodeQuery("admin_client_individual_links")), tu.InlineKeyboardButton(t.I18nBot("qrCode")).WithCallbackData(t.encodeQuery("admin_client_qr_links")), ), // TODOOOOOOOOOOOOOO: Add restart button here. ) numericKeyboardClient := tu.InlineKeyboard( tu.InlineKeyboardRow( tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.clientUsage")).WithCallbackData(t.encodeQuery("client_traffic")), tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.commands")).WithCallbackData(t.encodeQuery("client_commands")), ), tu.InlineKeyboardRow( tu.InlineKeyboardButton(t.I18nBot("pages.settings.subSettings")).WithCallbackData(t.encodeQuery("client_sub_links")), tu.InlineKeyboardButton(t.I18nBot("subscription.individualLinks")).WithCallbackData(t.encodeQuery("client_individual_links")), ), tu.InlineKeyboardRow( tu.InlineKeyboardButton(t.I18nBot("qrCode")).WithCallbackData(t.encodeQuery("client_qr_links")), ), ) var ReplyMarkup telego.ReplyMarkup if isAdmin { ReplyMarkup = numericKeyboard } else { ReplyMarkup = numericKeyboardClient } t.SendMsgToTgbot(chatId, msg, ReplyMarkup) } // SendMsgToTgbot sends a message to the Telegram bot with optional reply markup. func (t *Tgbot) SendMsgToTgbot(chatId int64, msg string, replyMarkup ...telego.ReplyMarkup) { if !isRunning { return } if msg == "" { logger.Info("[tgbot] message is empty!") return } var allMessages []string limit := 2000 // paging message if it is big if len(msg) > limit { messages := strings.Split(msg, "\r\n\r\n") lastIndex := -1 for _, message := range messages { if (len(allMessages) == 0) || (len(allMessages[lastIndex])+len(message) > limit) { allMessages = append(allMessages, message) lastIndex++ } else { allMessages[lastIndex] += "\r\n\r\n" + message } } if strings.TrimSpace(allMessages[len(allMessages)-1]) == "" { allMessages = allMessages[:len(allMessages)-1] } } else { allMessages = append(allMessages, msg) } for n, message := range allMessages { params := telego.SendMessageParams{ ChatID: tu.ID(chatId), Text: message, ParseMode: "HTML", } // only add replyMarkup to last message if len(replyMarkup) > 0 && n == (len(allMessages)-1) { params.ReplyMarkup = replyMarkup[0] } // Retry logic with exponential backoff for connection errors maxRetries := 3 for attempt := range maxRetries { ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) _, err := bot.SendMessage(ctx, ¶ms) cancel() if err == nil { break // Success } // Check if error is a connection error errStr := err.Error() isConnectionError := strings.Contains(errStr, "connection") || strings.Contains(errStr, "timeout") || strings.Contains(errStr, "closed") if isConnectionError && attempt < maxRetries-1 { // Exponential backoff: 1s, 2s, 4s backoff := time.Duration(1<" if subJsonURL != "" { msg += "\r\n\r\nJSON URL:\r\n" + subJsonURL + "" } inlineKeyboard := tu.InlineKeyboard( tu.InlineKeyboardRow( tu.InlineKeyboardButton(t.I18nBot("subscription.individualLinks")).WithCallbackData(t.encodeQuery("client_individual_links "+email)), ), tu.InlineKeyboardRow( tu.InlineKeyboardButton(t.I18nBot("qrCode")).WithCallbackData(t.encodeQuery("client_qr_links "+email)), ), ) t.SendMsgToTgbot(chatId, msg, inlineKeyboard) } // sendClientIndividualLinks fetches the subscription content (individual links) and sends it to the user func (t *Tgbot) sendClientIndividualLinks(chatId int64, email string) { // Build the HTML sub page URL; we'll call it with header Accept to get raw content subURL, _, err := t.buildSubscriptionURLs(email) if err != nil { t.SendMsgToTgbot(chatId, t.I18nBot("tgbot.answers.errorOperation")+"\r\n"+err.Error()) return } // Try to fetch raw subscription links. Prefer plain text response. req, err := http.NewRequest("GET", subURL, nil) if err != nil { t.SendMsgToTgbot(chatId, t.I18nBot("tgbot.answers.errorOperation")+"\r\n"+err.Error()) return } // Force plain text to avoid HTML page; controller respects Accept header req.Header.Set("Accept", "text/plain, */*;q=0.1") // Use optimized client with connection pooling ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() req = req.WithContext(ctx) resp, err := optimizedHTTPClient.Do(req) if err != nil { t.SendMsgToTgbot(chatId, t.I18nBot("tgbot.answers.errorOperation")+"\r\n"+err.Error()) return } defer resp.Body.Close() bodyBytes, err := io.ReadAll(resp.Body) if err != nil { t.SendMsgToTgbot(chatId, t.I18nBot("tgbot.answers.errorOperation")+"\r\n"+err.Error()) return } // If service is configured to encode (Base64), decode it encoded, _ := t.settingService.GetSubEncrypt() var content string if encoded { decoded, err := base64.StdEncoding.DecodeString(string(bodyBytes)) if err != nil { // fallback to raw text content = string(bodyBytes) } else { content = string(decoded) } } else { content = string(bodyBytes) } // Normalize line endings and trim lines := strings.Split(strings.ReplaceAll(content, "\r\n", "\n"), "\n") var cleaned []string for _, l := range lines { l = strings.TrimSpace(l) if l != "" { cleaned = append(cleaned, l) } } if len(cleaned) == 0 { t.SendMsgToTgbot(chatId, t.I18nBot("tgbot.noResult")) return } // Send in chunks to respect message length; use monospace formatting const maxPerMessage = 50 for i := 0; i < len(cleaned); i += maxPerMessage { j := i + maxPerMessage if j > len(cleaned) { j = len(cleaned) } chunk := cleaned[i:j] msg := t.I18nBot("subscription.individualLinks") + ":\r\n" for _, link := range chunk { // wrap each link in msg += "" + link + "\r\n" } t.SendMsgToTgbot(chatId, msg) } } // sendClientQRLinks generates QR images for subscription URL, JSON URL, and a few individual links, then sends them func (t *Tgbot) sendClientQRLinks(chatId int64, email string) { subURL, subJsonURL, err := t.buildSubscriptionURLs(email) if err != nil { t.SendMsgToTgbot(chatId, t.I18nBot("tgbot.answers.errorOperation")+"\r\n"+err.Error()) return } // Helper to create QR PNG bytes from content createQR := func(content string, size int) ([]byte, error) { if size <= 0 { size = 256 } return qrcode.Encode(content, qrcode.Medium, size) } // Inform user t.SendMsgToTgbot(chatId, "QRCode"+":") // Send sub URL QR (filename: sub.png) if png, err := createQR(subURL, 320); err == nil { document := tu.Document( tu.ID(chatId), tu.FileFromBytes(png, "sub.png"), ) _, _ = bot.SendDocument(context.Background(), document) } else { t.SendMsgToTgbot(chatId, t.I18nBot("tgbot.answers.errorOperation")+"\r\n"+err.Error()) } // Send JSON URL QR (filename: subjson.png) when available if subJsonURL != "" { if png, err := createQR(subJsonURL, 320); err == nil { document := tu.Document( tu.ID(chatId), tu.FileFromBytes(png, "subjson.png"), ) _, _ = bot.SendDocument(context.Background(), document) } else { t.SendMsgToTgbot(chatId, t.I18nBot("tgbot.answers.errorOperation")+"\r\n"+err.Error()) } } // Also generate a few individual links' QRs (first up to 5) subPageURL := subURL req, err := http.NewRequest("GET", subPageURL, nil) if err == nil { req.Header.Set("Accept", "text/plain, */*;q=0.1") ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() req = req.WithContext(ctx) if resp, err := optimizedHTTPClient.Do(req); err == nil { body, _ := io.ReadAll(resp.Body) _ = resp.Body.Close() encoded, _ := t.settingService.GetSubEncrypt() var content string if encoded { if dec, err := base64.StdEncoding.DecodeString(string(body)); err == nil { content = string(dec) } else { content = string(body) } } else { content = string(body) } lines := strings.Split(strings.ReplaceAll(content, "\r\n", "\n"), "\n") var cleaned []string for _, l := range lines { l = strings.TrimSpace(l) if l != "" { cleaned = append(cleaned, l) } } if len(cleaned) > 0 { max := min(len(cleaned), 5) for i := range max { if png, err := createQR(cleaned[i], 320); err == nil { // Use the email as filename for individual link QR filename := email + ".png" document := tu.Document( tu.ID(chatId), tu.FileFromBytes(png, filename), ) _, _ = bot.SendDocument(context.Background(), document) // Reduced delay for better performance if i < max-1 { // Only delay between documents, not after the last one time.Sleep(50 * time.Millisecond) } } } } } } } // SendMsgToTgbotAdmins sends a message to all admin Telegram chats. func (t *Tgbot) SendMsgToTgbotAdmins(msg string, replyMarkup ...telego.ReplyMarkup) { if len(replyMarkup) > 0 { for _, adminId := range adminIds { t.SendMsgToTgbot(adminId, msg, replyMarkup[0]) } } else { for _, adminId := range adminIds { t.SendMsgToTgbot(adminId, msg) } } } // SendReport sends a periodic report to admin chats. func (t *Tgbot) SendReport() { runTime, err := t.settingService.GetTgbotRuntime() if err == nil && len(runTime) > 0 { msg := "" msg += t.I18nBot("tgbot.messages.report", "RunTime=="+runTime) msg += t.I18nBot("tgbot.messages.datetime", "DateTime=="+time.Now().Format("2006-01-02 15:04:05")) t.SendMsgToTgbotAdmins(msg) } info := t.sendServerUsage() t.SendMsgToTgbotAdmins(info) t.sendExhaustedToAdmins() t.notifyExhausted() backupEnable, err := t.settingService.GetTgBotBackup() if err == nil && backupEnable { t.SendBackupToAdmins() } } // SendBackupToAdmins sends a database backup to admin chats. func (t *Tgbot) SendBackupToAdmins() { if !t.IsRunning() { return } for i, adminId := range adminIds { t.sendBackup(int64(adminId)) // Add delay between sends to avoid Telegram rate limits if i < len(adminIds)-1 { time.Sleep(1 * time.Second) } } } // sendExhaustedToAdmins sends notifications about exhausted clients to admins. func (t *Tgbot) sendExhaustedToAdmins() { if !t.IsRunning() { return } for _, adminId := range adminIds { t.getExhausted(int64(adminId)) } } // getServerUsage retrieves and formats server usage information. func (t *Tgbot) getServerUsage(chatId int64, messageID ...int) string { info := t.prepareServerUsageInfo() keyboard := tu.InlineKeyboard(tu.InlineKeyboardRow( tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.refresh")).WithCallbackData(t.encodeQuery("usage_refresh")))) if len(messageID) > 0 { t.editMessageTgBot(chatId, messageID[0], info, keyboard) } else { t.SendMsgToTgbot(chatId, info, keyboard) } return info } // Send server usage without an inline keyboard func (t *Tgbot) sendServerUsage() string { info := t.prepareServerUsageInfo() return info } // prepareServerUsageInfo prepares the server usage information string. func (t *Tgbot) prepareServerUsageInfo() string { // Check if we have cached data first if cachedStats, found := t.getCachedServerStats(); found { return cachedStats } info, ipv4, ipv6 := "", "", "" // get latest status of server with caching if cachedStatus, found := t.getCachedStatus(); found { t.lastStatus = cachedStatus } else { t.lastStatus = t.serverService.GetStatus(t.lastStatus) t.setCachedStatus(t.lastStatus) } onlines := p.GetOnlineClients() info += t.I18nBot("tgbot.messages.hostname", "Hostname=="+hostname) info += t.I18nBot("tgbot.messages.version", "Version=="+config.GetVersion()) info += t.I18nBot("tgbot.messages.xrayVersion", "XrayVersion=="+fmt.Sprint(t.lastStatus.Xray.Version)) // get ip address netInterfaces, err := net.Interfaces() if err != nil { logger.Error("net.Interfaces failed, err: ", err.Error()) info += t.I18nBot("tgbot.messages.ip", "IP=="+t.I18nBot("tgbot.unknown")) info += "\r\n" } else { for i := range netInterfaces { if (netInterfaces[i].Flags & net.FlagUp) != 0 { addrs, _ := netInterfaces[i].Addrs() for _, address := range addrs { if ipnet, ok := address.(*net.IPNet); ok && !ipnet.IP.IsLoopback() { if ipnet.IP.To4() != nil { ipv4 += ipnet.IP.String() + " " } else if ipnet.IP.To16() != nil && !ipnet.IP.IsLinkLocalUnicast() { ipv6 += ipnet.IP.String() + " " } } } } } info += t.I18nBot("tgbot.messages.ipv4", "IPv4=="+ipv4) info += t.I18nBot("tgbot.messages.ipv6", "IPv6=="+ipv6) } info += t.I18nBot("tgbot.messages.serverUpTime", "UpTime=="+strconv.FormatUint(t.lastStatus.Uptime/86400, 10), "Unit=="+t.I18nBot("tgbot.days")) info += t.I18nBot("tgbot.messages.serverLoad", "Load1=="+strconv.FormatFloat(t.lastStatus.Loads[0], 'f', 2, 64), "Load2=="+strconv.FormatFloat(t.lastStatus.Loads[1], 'f', 2, 64), "Load3=="+strconv.FormatFloat(t.lastStatus.Loads[2], 'f', 2, 64)) info += t.I18nBot("tgbot.messages.serverMemory", "Current=="+common.FormatTraffic(int64(t.lastStatus.Mem.Current)), "Total=="+common.FormatTraffic(int64(t.lastStatus.Mem.Total))) info += t.I18nBot("tgbot.messages.onlinesCount", "Count=="+fmt.Sprint(len(onlines))) info += t.I18nBot("tgbot.messages.tcpCount", "Count=="+strconv.Itoa(t.lastStatus.TcpCount)) info += t.I18nBot("tgbot.messages.udpCount", "Count=="+strconv.Itoa(t.lastStatus.UdpCount)) info += t.I18nBot("tgbot.messages.traffic", "Total=="+common.FormatTraffic(int64(t.lastStatus.NetTraffic.Sent+t.lastStatus.NetTraffic.Recv)), "Upload=="+common.FormatTraffic(int64(t.lastStatus.NetTraffic.Sent)), "Download=="+common.FormatTraffic(int64(t.lastStatus.NetTraffic.Recv))) info += t.I18nBot("tgbot.messages.xrayStatus", "State=="+fmt.Sprint(t.lastStatus.Xray.State)) // Cache the complete server stats t.setCachedServerStats(info) return info } // UserLoginNotify sends a notification about user login attempts to admins. func (t *Tgbot) UserLoginNotify(username string, password string, ip string, time string, status LoginStatus) { if !t.IsRunning() { return } if username == "" || ip == "" || time == "" { logger.Warning("UserLoginNotify failed, invalid info!") return } loginNotifyEnabled, err := t.settingService.GetTgBotLoginNotify() if err != nil || !loginNotifyEnabled { return } msg := "" switch status { case LoginSuccess: msg += t.I18nBot("tgbot.messages.loginSuccess") msg += t.I18nBot("tgbot.messages.hostname", "Hostname=="+hostname) case LoginFail: msg += t.I18nBot("tgbot.messages.loginFailed") msg += t.I18nBot("tgbot.messages.hostname", "Hostname=="+hostname) msg += t.I18nBot("tgbot.messages.password", "Password=="+password) } msg += t.I18nBot("tgbot.messages.username", "Username=="+username) msg += t.I18nBot("tgbot.messages.ip", "IP=="+ip) msg += t.I18nBot("tgbot.messages.time", "Time=="+time) t.SendMsgToTgbotAdmins(msg) } // getInboundUsages retrieves and formats inbound usage information. func (t *Tgbot) getInboundUsages() string { var info strings.Builder // get traffic inbounds, err := t.inboundService.GetAllInbounds() if err != nil { logger.Warning("GetAllInbounds run failed:", err) info.WriteString(t.I18nBot("tgbot.answers.getInboundsFailed")) } else { // NOTE:If there no any sessions here,need to notify here // TODO:Sub-node push, automatic conversion format for _, inbound := range inbounds { info.WriteString(t.I18nBot("tgbot.messages.inbound", "Remark=="+inbound.Remark)) info.WriteString(t.I18nBot("tgbot.messages.port", "Port=="+strconv.Itoa(inbound.Port))) info.WriteString(t.I18nBot("tgbot.messages.traffic", "Total=="+common.FormatTraffic((inbound.Up+inbound.Down)), "Upload=="+common.FormatTraffic(inbound.Up), "Download=="+common.FormatTraffic(inbound.Down))) if inbound.ExpiryTime == 0 { info.WriteString(t.I18nBot("tgbot.messages.expire", "Time=="+t.I18nBot("tgbot.unlimited"))) } else { info.WriteString(t.I18nBot("tgbot.messages.expire", "Time=="+time.Unix((inbound.ExpiryTime/1000), 0).Format("2006-01-02 15:04:05"))) } info.WriteString("\r\n") } } return info.String() } // getInbounds creates an inline keyboard with all inbounds. func (t *Tgbot) getInbounds() (*telego.InlineKeyboardMarkup, error) { inbounds, err := t.inboundService.GetAllInbounds() if err != nil { logger.Warning("GetAllInbounds run failed:", err) return nil, errors.New(t.I18nBot("tgbot.answers.getInboundsFailed")) } if len(inbounds) == 0 { logger.Warning("No inbounds found") return nil, errors.New(t.I18nBot("tgbot.answers.getInboundsFailed")) } var buttons []telego.InlineKeyboardButton for _, inbound := range inbounds { status := "❌" if inbound.Enable { status = "✅" } callbackData := t.encodeQuery(fmt.Sprintf("%s %d", "get_clients", inbound.Id)) buttons = append(buttons, tu.InlineKeyboardButton(fmt.Sprintf("%v - %v", inbound.Remark, status)).WithCallbackData(callbackData)) } cols := 1 if len(buttons) >= 6 { cols = 2 } keyboard := tu.InlineKeyboardGrid(tu.InlineKeyboardCols(cols, buttons...)) return keyboard, nil } // getInboundsFor builds an inline keyboard of inbounds for a custom next action. func (t *Tgbot) getInboundsFor(nextAction string) (*telego.InlineKeyboardMarkup, error) { inbounds, err := t.inboundService.GetAllInbounds() if err != nil { logger.Warning("GetAllInbounds run failed:", err) return nil, errors.New(t.I18nBot("tgbot.answers.getInboundsFailed")) } if len(inbounds) == 0 { logger.Warning("No inbounds found") return nil, errors.New(t.I18nBot("tgbot.answers.getInboundsFailed")) } var buttons []telego.InlineKeyboardButton for _, inbound := range inbounds { status := "❌" if inbound.Enable { status = "✅" } callbackData := t.encodeQuery(fmt.Sprintf("%s %d", nextAction, inbound.Id)) buttons = append(buttons, tu.InlineKeyboardButton(fmt.Sprintf("%v - %v", inbound.Remark, status)).WithCallbackData(callbackData)) } cols := 1 if len(buttons) >= 6 { cols = 2 } keyboard := tu.InlineKeyboardGrid(tu.InlineKeyboardCols(cols, buttons...)) return keyboard, nil } // getInboundClientsFor lists clients of an inbound with a specific action prefix to be appended with email func (t *Tgbot) getInboundClientsFor(inboundID int, action string) (*telego.InlineKeyboardMarkup, error) { inbound, err := t.inboundService.GetInbound(inboundID) if err != nil { logger.Warning("getInboundClientsFor run failed:", err) return nil, errors.New(t.I18nBot("tgbot.answers.getInboundsFailed")) } clients, err := t.inboundService.GetClients(inbound) var buttons []telego.InlineKeyboardButton if err != nil { logger.Warning("GetInboundClients run failed:", err) return nil, errors.New(t.I18nBot("tgbot.answers.getInboundsFailed")) } else { if len(clients) > 0 { for _, client := range clients { buttons = append(buttons, tu.InlineKeyboardButton(client.Email).WithCallbackData(t.encodeQuery(action+" "+client.Email))) } } else { return nil, errors.New(t.I18nBot("tgbot.answers.getClientsFailed")) } } cols := 0 if len(buttons) < 6 { cols = 3 } else { cols = 2 } keyboard := tu.InlineKeyboardGrid(tu.InlineKeyboardCols(cols, buttons...)) return keyboard, nil } // getInboundsAddClient creates an inline keyboard for adding clients to inbounds. func (t *Tgbot) getInboundsAddClient() (*telego.InlineKeyboardMarkup, error) { inbounds, err := t.inboundService.GetAllInbounds() if err != nil { logger.Warning("GetAllInbounds run failed:", err) return nil, errors.New(t.I18nBot("tgbot.answers.getInboundsFailed")) } if len(inbounds) == 0 { logger.Warning("No inbounds found") return nil, errors.New(t.I18nBot("tgbot.answers.getInboundsFailed")) } excludedProtocols := map[model.Protocol]bool{ model.Tunnel: true, model.Mixed: true, model.WireGuard: true, model.HTTP: true, } var buttons []telego.InlineKeyboardButton for _, inbound := range inbounds { if excludedProtocols[inbound.Protocol] { continue } status := "❌" if inbound.Enable { status = "✅" } callbackData := t.encodeQuery(fmt.Sprintf("%s %d", "add_client_to", inbound.Id)) buttons = append(buttons, tu.InlineKeyboardButton(fmt.Sprintf("%v - %v", inbound.Remark, status)).WithCallbackData(callbackData)) } cols := 1 if len(buttons) >= 6 { cols = 2 } keyboard := tu.InlineKeyboardGrid(tu.InlineKeyboardCols(cols, buttons...)) return keyboard, nil } // getInboundClients creates an inline keyboard with clients of a specific inbound. func (t *Tgbot) getInboundClients(id int) (*telego.InlineKeyboardMarkup, error) { inbound, err := t.inboundService.GetInbound(id) if err != nil { logger.Warning("getIboundClients run failed:", err) return nil, errors.New(t.I18nBot("tgbot.answers.getInboundsFailed")) } clients, err := t.inboundService.GetClients(inbound) var buttons []telego.InlineKeyboardButton if err != nil { logger.Warning("GetInboundClients run failed:", err) return nil, errors.New(t.I18nBot("tgbot.answers.getInboundsFailed")) } else { if len(clients) > 0 { for _, client := range clients { buttons = append(buttons, tu.InlineKeyboardButton(client.Email).WithCallbackData(t.encodeQuery("client_get_usage "+client.Email))) } } else { return nil, errors.New(t.I18nBot("tgbot.answers.getClientsFailed")) } } cols := 0 if len(buttons) < 6 { cols = 3 } else { cols = 2 } keyboard := tu.InlineKeyboardGrid(tu.InlineKeyboardCols(cols, buttons...)) return keyboard, nil } // clientInfoMsg formats client information message based on traffic and flags. func (t *Tgbot) clientInfoMsg( traffic *xray.ClientTraffic, printEnabled bool, printOnline bool, printActive bool, printDate bool, printTraffic bool, printRefreshed bool, ) string { now := time.Now().Unix() expiryTime := "" flag := false diff := traffic.ExpiryTime/1000 - now if traffic.ExpiryTime == 0 { expiryTime = t.I18nBot("tgbot.unlimited") } else if diff > 172800 || !traffic.Enable { expiryTime = time.Unix((traffic.ExpiryTime / 1000), 0).Format("2006-01-02 15:04:05") if diff > 0 { days := diff / 86400 hours := (diff % 86400) / 3600 minutes := (diff % 3600) / 60 remainingTime := "" if days > 0 { remainingTime += fmt.Sprintf("%d %s ", days, t.I18nBot("tgbot.days")) } if hours > 0 { remainingTime += fmt.Sprintf("%d %s ", hours, t.I18nBot("tgbot.hours")) } if minutes > 0 { remainingTime += fmt.Sprintf("%d %s", minutes, t.I18nBot("tgbot.minutes")) } expiryTime += fmt.Sprintf(" (%s)", remainingTime) } } else if traffic.ExpiryTime < 0 { expiryTime = fmt.Sprintf("%d %s", traffic.ExpiryTime/-86400000, t.I18nBot("tgbot.days")) flag = true } else { expiryTime = fmt.Sprintf("%d %s", diff/3600, t.I18nBot("tgbot.hours")) flag = true } total := "" if traffic.Total == 0 { total = t.I18nBot("tgbot.unlimited") } else { total = common.FormatTraffic((traffic.Total)) } enabled := "" isEnabled, err := t.inboundService.checkIsEnabledByEmail(traffic.Email) if err != nil { logger.Warning(err) enabled = t.I18nBot("tgbot.wentWrong") } else if isEnabled { enabled = t.I18nBot("tgbot.messages.yes") } else { enabled = t.I18nBot("tgbot.messages.no") } active := "" if traffic.Enable { active = t.I18nBot("tgbot.messages.yes") } else { active = t.I18nBot("tgbot.messages.no") } status := t.I18nBot("tgbot.offline") isOnline := false if p.IsRunning() { if slices.Contains(p.GetOnlineClients(), traffic.Email) { status = t.I18nBot("tgbot.online") isOnline = true } } output := "" output += t.I18nBot("tgbot.messages.email", "Email=="+traffic.Email) if printEnabled { output += t.I18nBot("tgbot.messages.enabled", "Enable=="+enabled) } if printOnline { output += t.I18nBot("tgbot.messages.online", "Status=="+status) if !isOnline && traffic.LastOnline > 0 { output += t.I18nBot("tgbot.messages.lastOnline", "Time=="+time.UnixMilli(traffic.LastOnline).Format("2006-01-02 15:04:05")) } } if printActive { output += t.I18nBot("tgbot.messages.active", "Enable=="+active) } if printDate { if flag { output += t.I18nBot("tgbot.messages.expireIn", "Time=="+expiryTime) } else { output += t.I18nBot("tgbot.messages.expire", "Time=="+expiryTime) } } if printTraffic { output += t.I18nBot("tgbot.messages.upload", "Upload=="+common.FormatTraffic(traffic.Up)) output += t.I18nBot("tgbot.messages.download", "Download=="+common.FormatTraffic(traffic.Down)) output += t.I18nBot("tgbot.messages.total", "UpDown=="+common.FormatTraffic((traffic.Up+traffic.Down)), "Total=="+total) } if printRefreshed { output += t.I18nBot("tgbot.messages.refreshedOn", "Time=="+time.Now().Format("2006-01-02 15:04:05")) } return output } // getClientUsage retrieves and sends client usage information to the chat. func (t *Tgbot) getClientUsage(chatId int64, tgUserID int64, email ...string) { traffics, err := t.inboundService.GetClientTrafficTgBot(tgUserID) if err != nil { logger.Warning(err) msg := t.I18nBot("tgbot.wentWrong") t.SendMsgToTgbot(chatId, msg) return } if len(traffics) == 0 { t.SendMsgToTgbot(chatId, t.I18nBot("tgbot.answers.askToAddUserId", "TgUserID=="+strconv.FormatInt(tgUserID, 10))) return } output := "" if len(traffics) > 0 { if len(email) > 0 { for _, traffic := range traffics { if traffic.Email == email[0] { output := t.clientInfoMsg(traffic, true, true, true, true, true, true) t.SendMsgToTgbot(chatId, output) return } } msg := t.I18nBot("tgbot.noResult") t.SendMsgToTgbot(chatId, msg) return } else { for _, traffic := range traffics { output += t.clientInfoMsg(traffic, true, true, true, true, true, false) output += "\r\n" } } } output += t.I18nBot("tgbot.messages.refreshedOn", "Time=="+time.Now().Format("2006-01-02 15:04:05")) t.SendMsgToTgbot(chatId, output) output = t.I18nBot("tgbot.commands.pleaseChoose") t.SendAnswer(chatId, output, false) } // searchClientIps searches and sends client IP addresses for the given email. func (t *Tgbot) searchClientIps(chatId int64, email string, messageID ...int) { ips, err := t.inboundService.GetInboundClientIps(email) if err != nil || len(ips) == 0 { ips = t.I18nBot("tgbot.noIpRecord") } formattedIps := ips if err == nil && len(ips) > 0 { type ipWithTimestamp struct { IP string `json:"ip"` Timestamp int64 `json:"timestamp"` } var ipsWithTime []ipWithTimestamp if json.Unmarshal([]byte(ips), &ipsWithTime) == nil && len(ipsWithTime) > 0 { lines := make([]string, 0, len(ipsWithTime)) for _, item := range ipsWithTime { if item.IP == "" { continue } if item.Timestamp > 0 { ts := time.Unix(item.Timestamp, 0).Format("2006-01-02 15:04:05") lines = append(lines, fmt.Sprintf("%s (%s)", item.IP, ts)) continue } lines = append(lines, item.IP) } if len(lines) > 0 { formattedIps = strings.Join(lines, "\n") } } else { var oldIps []string if json.Unmarshal([]byte(ips), &oldIps) == nil && len(oldIps) > 0 { formattedIps = strings.Join(oldIps, "\n") } } } output := "" output += t.I18nBot("tgbot.messages.email", "Email=="+email) output += t.I18nBot("tgbot.messages.ips", "IPs=="+formattedIps) output += t.I18nBot("tgbot.messages.refreshedOn", "Time=="+time.Now().Format("2006-01-02 15:04:05")) inlineKeyboard := tu.InlineKeyboard( tu.InlineKeyboardRow( tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.refresh")).WithCallbackData(t.encodeQuery("ips_refresh "+email)), ), tu.InlineKeyboardRow( tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.clearIPs")).WithCallbackData(t.encodeQuery("clear_ips "+email)), ), ) if len(messageID) > 0 { t.editMessageTgBot(chatId, messageID[0], output, inlineKeyboard) } else { t.SendMsgToTgbot(chatId, output, inlineKeyboard) } } // clientTelegramUserInfo retrieves and sends Telegram user info for the client. func (t *Tgbot) clientTelegramUserInfo(chatId int64, email string, messageID ...int) { traffic, client, err := t.inboundService.GetClientByEmail(email) if err != nil { logger.Warning(err) msg := t.I18nBot("tgbot.wentWrong") t.SendMsgToTgbot(chatId, msg) return } if client == nil { msg := t.I18nBot("tgbot.noResult") t.SendMsgToTgbot(chatId, msg) return } tgId := "None" if client.TgID != 0 { tgId = strconv.FormatInt(client.TgID, 10) } output := "" output += t.I18nBot("tgbot.messages.email", "Email=="+email) output += t.I18nBot("tgbot.messages.TGUser", "TelegramID=="+tgId) output += t.I18nBot("tgbot.messages.refreshedOn", "Time=="+time.Now().Format("2006-01-02 15:04:05")) inlineKeyboard := tu.InlineKeyboard( tu.InlineKeyboardRow( tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.refresh")).WithCallbackData(t.encodeQuery("tgid_refresh "+email)), ), tu.InlineKeyboardRow( tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.removeTGUser")).WithCallbackData(t.encodeQuery("tgid_remove "+email)), ), ) if len(messageID) > 0 { t.editMessageTgBot(chatId, messageID[0], output, inlineKeyboard) } else { t.SendMsgToTgbot(chatId, output, inlineKeyboard) requestUser := telego.KeyboardButtonRequestUsers{ RequestID: int32(traffic.Id), UserIsBot: new(bool), } keyboard := tu.Keyboard( tu.KeyboardRow( tu.KeyboardButton(t.I18nBot("tgbot.buttons.selectTGUser")).WithRequestUsers(&requestUser), ), tu.KeyboardRow( tu.KeyboardButton(t.I18nBot("tgbot.buttons.closeKeyboard")), ), ).WithIsPersistent().WithResizeKeyboard() t.SendMsgToTgbot(chatId, t.I18nBot("tgbot.buttons.selectOneTGUser"), keyboard) } } // searchClient searches for a client by email and sends the information. func (t *Tgbot) searchClient(chatId int64, email string, messageID ...int) { traffic, err := t.inboundService.GetClientTrafficByEmail(email) if err != nil { logger.Warning(err) msg := t.I18nBot("tgbot.wentWrong") t.SendMsgToTgbot(chatId, msg) return } if traffic == nil { msg := t.I18nBot("tgbot.noResult") t.SendMsgToTgbot(chatId, msg) return } output := t.clientInfoMsg(traffic, true, true, true, true, true, true) inlineKeyboard := tu.InlineKeyboard( tu.InlineKeyboardRow( tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.refresh")).WithCallbackData(t.encodeQuery("client_refresh "+email)), ), tu.InlineKeyboardRow( tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.resetTraffic")).WithCallbackData(t.encodeQuery("reset_traffic "+email)), tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.limitTraffic")).WithCallbackData(t.encodeQuery("limit_traffic "+email)), ), tu.InlineKeyboardRow( tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.resetExpire")).WithCallbackData(t.encodeQuery("reset_exp "+email)), ), tu.InlineKeyboardRow( tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.ipLog")).WithCallbackData(t.encodeQuery("ip_log "+email)), tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.ipLimit")).WithCallbackData(t.encodeQuery("ip_limit "+email)), ), tu.InlineKeyboardRow( tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.setTGUser")).WithCallbackData(t.encodeQuery("tg_user "+email)), ), tu.InlineKeyboardRow( tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.toggle")).WithCallbackData(t.encodeQuery("toggle_enable "+email)), ), ) if len(messageID) > 0 { t.editMessageTgBot(chatId, messageID[0], output, inlineKeyboard) } else { t.SendMsgToTgbot(chatId, output, inlineKeyboard) } } // getCommonClientButtons returns the shared inline keyboard rows for client configuration func (t *Tgbot) getCommonClientButtons() [][]telego.InlineKeyboardButton { return [][]telego.InlineKeyboardButton{ tu.InlineKeyboardRow( tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.limitTraffic")).WithCallbackData("add_client_ch_default_traffic"), tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.resetExpire")).WithCallbackData("add_client_ch_default_exp"), ), tu.InlineKeyboardRow( tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.change_comment")).WithCallbackData("add_client_ch_default_comment"), tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.ipLimit")).WithCallbackData("add_client_ch_default_ip_limit"), ), tu.InlineKeyboardRow( tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.submitDisable")).WithCallbackData("add_client_submit_disable"), tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.submitEnable")).WithCallbackData("add_client_submit_enable"), ), tu.InlineKeyboardRow( tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.cancel")).WithCallbackData("add_client_cancel"), ), } } // addClient handles the process of adding a new client to an inbound. func (t *Tgbot) addClient(chatId int64, msg string, messageID ...int) { inbound, err := t.inboundService.GetInbound(receiver_inbound_ID) if err != nil { t.SendMsgToTgbot(chatId, err.Error()) return } protocol := inbound.Protocol var protocolRows [][]telego.InlineKeyboardButton switch protocol { case model.VMESS, model.VLESS: protocolRows = [][]telego.InlineKeyboardButton{ tu.InlineKeyboardRow( tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.change_email")).WithCallbackData("add_client_ch_default_email"), tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.change_id")).WithCallbackData("add_client_ch_default_id"), ), } case model.Trojan: protocolRows = [][]telego.InlineKeyboardButton{ tu.InlineKeyboardRow( tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.change_email")).WithCallbackData("add_client_ch_default_email"), tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.change_password")).WithCallbackData("add_client_ch_default_pass_tr"), ), } case model.Shadowsocks: protocolRows = [][]telego.InlineKeyboardButton{ tu.InlineKeyboardRow( tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.change_email")).WithCallbackData("add_client_ch_default_email"), tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.change_password")).WithCallbackData("add_client_ch_default_pass_sh"), ), } } commonRows := t.getCommonClientButtons() inlineKeyboard := tu.InlineKeyboard(append(protocolRows, commonRows...)...) if len(messageID) > 0 { t.editMessageTgBot(chatId, messageID[0], msg, inlineKeyboard) } else { t.SendMsgToTgbot(chatId, msg, inlineKeyboard) } } // searchInbound searches for inbounds by remark and sends the results. func (t *Tgbot) searchInbound(chatId int64, remark string) { inbounds, err := t.inboundService.SearchInbounds(remark) if err != nil { logger.Warning(err) msg := t.I18nBot("tgbot.wentWrong") t.SendMsgToTgbot(chatId, msg) return } if len(inbounds) == 0 { msg := t.I18nBot("tgbot.noInbounds") t.SendMsgToTgbot(chatId, msg) return } for _, inbound := range inbounds { info := "" info += t.I18nBot("tgbot.messages.inbound", "Remark=="+inbound.Remark) info += t.I18nBot("tgbot.messages.port", "Port=="+strconv.Itoa(inbound.Port)) info += t.I18nBot("tgbot.messages.traffic", "Total=="+common.FormatTraffic((inbound.Up+inbound.Down)), "Upload=="+common.FormatTraffic(inbound.Up), "Download=="+common.FormatTraffic(inbound.Down)) if inbound.ExpiryTime == 0 { info += t.I18nBot("tgbot.messages.expire", "Time=="+t.I18nBot("tgbot.unlimited")) } else { info += t.I18nBot("tgbot.messages.expire", "Time=="+time.Unix((inbound.ExpiryTime/1000), 0).Format("2006-01-02 15:04:05")) } t.SendMsgToTgbot(chatId, info) if len(inbound.ClientStats) > 0 { var output strings.Builder for _, traffic := range inbound.ClientStats { output.WriteString(t.clientInfoMsg(&traffic, true, true, true, true, true, true)) } t.SendMsgToTgbot(chatId, output.String()) } } } // getExhausted retrieves and sends information about exhausted clients. func (t *Tgbot) getExhausted(chatId int64) { trDiff := int64(0) exDiff := int64(0) now := time.Now().Unix() * 1000 var exhaustedInbounds []model.Inbound var exhaustedClients []xray.ClientTraffic var disabledInbounds []model.Inbound var disabledClients []xray.ClientTraffic TrafficThreshold, err := t.settingService.GetTrafficDiff() if err == nil && TrafficThreshold > 0 { trDiff = int64(TrafficThreshold) * 1073741824 } ExpireThreshold, err := t.settingService.GetExpireDiff() if err == nil && ExpireThreshold > 0 { exDiff = int64(ExpireThreshold) * 86400000 } inbounds, err := t.inboundService.GetAllInbounds() if err != nil { logger.Warning("Unable to load Inbounds", err) } for _, inbound := range inbounds { if inbound.Enable { if (inbound.ExpiryTime > 0 && (inbound.ExpiryTime-now < exDiff)) || (inbound.Total > 0 && (inbound.Total-(inbound.Up+inbound.Down) < trDiff)) { exhaustedInbounds = append(exhaustedInbounds, *inbound) } if len(inbound.ClientStats) > 0 { for _, client := range inbound.ClientStats { if client.Enable { if (client.ExpiryTime > 0 && (client.ExpiryTime-now < exDiff)) || (client.Total > 0 && (client.Total-(client.Up+client.Down) < trDiff)) { exhaustedClients = append(exhaustedClients, client) } } else { disabledClients = append(disabledClients, client) } } } } else { disabledInbounds = append(disabledInbounds, *inbound) } } // Inbounds output := "" output += t.I18nBot("tgbot.messages.exhaustedCount", "Type=="+t.I18nBot("tgbot.inbounds")) output += t.I18nBot("tgbot.messages.disabled", "Disabled=="+strconv.Itoa(len(disabledInbounds))) output += t.I18nBot("tgbot.messages.depleteSoon", "Deplete=="+strconv.Itoa(len(exhaustedInbounds))) if len(exhaustedInbounds) > 0 { output += t.I18nBot("tgbot.messages.depleteSoon", "Deplete=="+t.I18nBot("tgbot.inbounds")) for _, inbound := range exhaustedInbounds { output += t.I18nBot("tgbot.messages.inbound", "Remark=="+inbound.Remark) output += t.I18nBot("tgbot.messages.port", "Port=="+strconv.Itoa(inbound.Port)) output += t.I18nBot("tgbot.messages.traffic", "Total=="+common.FormatTraffic((inbound.Up+inbound.Down)), "Upload=="+common.FormatTraffic(inbound.Up), "Download=="+common.FormatTraffic(inbound.Down)) if inbound.ExpiryTime == 0 { output += t.I18nBot("tgbot.messages.expire", "Time=="+t.I18nBot("tgbot.unlimited")) } else { output += t.I18nBot("tgbot.messages.expire", "Time=="+time.Unix((inbound.ExpiryTime/1000), 0).Format("2006-01-02 15:04:05")) } output += "\r\n" } } // Clients exhaustedCC := len(exhaustedClients) output += t.I18nBot("tgbot.messages.exhaustedCount", "Type=="+t.I18nBot("tgbot.clients")) output += t.I18nBot("tgbot.messages.disabled", "Disabled=="+strconv.Itoa(len(disabledClients))) output += t.I18nBot("tgbot.messages.depleteSoon", "Deplete=="+strconv.Itoa(exhaustedCC)) if exhaustedCC > 0 { output += t.I18nBot("tgbot.messages.depleteSoon", "Deplete=="+t.I18nBot("tgbot.clients")) var buttons []telego.InlineKeyboardButton for _, traffic := range exhaustedClients { output += t.clientInfoMsg(&traffic, true, false, false, true, true, false) output += "\r\n" buttons = append(buttons, tu.InlineKeyboardButton(traffic.Email).WithCallbackData(t.encodeQuery("client_get_usage "+traffic.Email))) } cols := 0 if exhaustedCC < 11 { cols = 1 } else { cols = 2 } output += t.I18nBot("tgbot.messages.refreshedOn", "Time=="+time.Now().Format("2006-01-02 15:04:05")) keyboard := tu.InlineKeyboardGrid(tu.InlineKeyboardCols(cols, buttons...)) t.SendMsgToTgbot(chatId, output, keyboard) } else { output += t.I18nBot("tgbot.messages.refreshedOn", "Time=="+time.Now().Format("2006-01-02 15:04:05")) t.SendMsgToTgbot(chatId, output) } } // notifyExhausted sends notifications for exhausted clients. func (t *Tgbot) notifyExhausted() { trDiff := int64(0) exDiff := int64(0) now := time.Now().Unix() * 1000 TrafficThreshold, err := t.settingService.GetTrafficDiff() if err == nil && TrafficThreshold > 0 { trDiff = int64(TrafficThreshold) * 1073741824 } ExpireThreshold, err := t.settingService.GetExpireDiff() if err == nil && ExpireThreshold > 0 { exDiff = int64(ExpireThreshold) * 86400000 } inbounds, err := t.inboundService.GetAllInbounds() if err != nil { logger.Warning("Unable to load Inbounds", err) } var chatIDsDone []int64 for _, inbound := range inbounds { if inbound.Enable { if len(inbound.ClientStats) > 0 { clients, err := t.inboundService.GetClients(inbound) if err == nil { for _, client := range clients { if client.TgID != 0 { chatID := client.TgID if !int64Contains(chatIDsDone, chatID) && !checkAdmin(chatID) { var disabledClients []xray.ClientTraffic var exhaustedClients []xray.ClientTraffic traffics, err := t.inboundService.GetClientTrafficTgBot(client.TgID) if err == nil && len(traffics) > 0 { output := t.I18nBot("tgbot.messages.exhaustedCount", "Type=="+t.I18nBot("tgbot.clients")) for _, traffic := range traffics { if traffic.Enable { if (traffic.ExpiryTime > 0 && (traffic.ExpiryTime-now < exDiff)) || (traffic.Total > 0 && (traffic.Total-(traffic.Up+traffic.Down) < trDiff)) { exhaustedClients = append(exhaustedClients, *traffic) } } else { disabledClients = append(disabledClients, *traffic) } } if len(exhaustedClients) > 0 { output += t.I18nBot("tgbot.messages.disabled", "Disabled=="+strconv.Itoa(len(disabledClients))) if len(disabledClients) > 0 { output += t.I18nBot("tgbot.clients") + ":\r\n" for _, traffic := range disabledClients { output += " " + traffic.Email } output += "\r\n" } output += "\r\n" output += t.I18nBot("tgbot.messages.depleteSoon", "Deplete=="+strconv.Itoa(len(exhaustedClients))) for _, traffic := range exhaustedClients { output += t.clientInfoMsg(&traffic, true, false, false, true, true, false) output += "\r\n" } t.SendMsgToTgbot(chatID, output) } chatIDsDone = append(chatIDsDone, chatID) } } } } } } } } } // int64Contains checks if an int64 slice contains a specific item. func int64Contains(slice []int64, item int64) bool { for _, s := range slice { if s == item { return true } } return false } // onlineClients retrieves and sends information about online clients. func (t *Tgbot) onlineClients(chatId int64, messageID ...int) { if !p.IsRunning() { return } onlines := p.GetOnlineClients() onlinesCount := len(onlines) output := t.I18nBot("tgbot.messages.onlinesCount", "Count=="+fmt.Sprint(onlinesCount)) keyboard := tu.InlineKeyboard(tu.InlineKeyboardRow( tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.refresh")).WithCallbackData(t.encodeQuery("onlines_refresh")))) if onlinesCount > 0 { var buttons []telego.InlineKeyboardButton for _, online := range onlines { buttons = append(buttons, tu.InlineKeyboardButton(online).WithCallbackData(t.encodeQuery("client_get_usage "+online))) } cols := 0 if onlinesCount < 21 { cols = 2 } else if onlinesCount < 61 { cols = 3 } else { cols = 4 } keyboard.InlineKeyboard = append(keyboard.InlineKeyboard, tu.InlineKeyboardCols(cols, buttons...)...) } if len(messageID) > 0 { t.editMessageTgBot(chatId, messageID[0], output, keyboard) } else { t.SendMsgToTgbot(chatId, output, keyboard) } } // sendBackup sends a backup of the database and configuration files. func (t *Tgbot) sendBackup(chatId int64) { output := t.I18nBot("tgbot.messages.backupTime", "Time=="+time.Now().Format("2006-01-02 15:04:05")) t.SendMsgToTgbot(chatId, output) // Update by manually trigger a checkpoint operation err := database.Checkpoint() if err != nil { logger.Error("Error in trigger a checkpoint operation: ", err) } // Send database backup file, err := os.Open(config.GetDBPath()) if err == nil { defer file.Close() ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() document := tu.Document( tu.ID(chatId), tu.File(file), ) _, err = bot.SendDocument(ctx, document) if err != nil { logger.Error("Error in uploading backup: ", err) } } else { logger.Error("Error in opening db file for backup: ", err) } // Small delay between file sends time.Sleep(500 * time.Millisecond) // Send config.json backup file, err = os.Open(xray.GetConfigPath()) if err == nil { defer file.Close() ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() document := tu.Document( tu.ID(chatId), tu.File(file), ) _, err = bot.SendDocument(ctx, document) if err != nil { logger.Error("Error in uploading config.json: ", err) } } else { logger.Error("Error in opening config.json file for backup: ", err) } } // sendBanLogs sends the ban logs to the specified chat. func (t *Tgbot) sendBanLogs(chatId int64, dt bool) { if dt { output := t.I18nBot("tgbot.messages.datetime", "DateTime=="+time.Now().Format("2006-01-02 15:04:05")) t.SendMsgToTgbot(chatId, output) } file, err := os.Open(xray.GetIPLimitBannedPrevLogPath()) if err == nil { // Check if the file is non-empty before attempting to upload fileInfo, _ := file.Stat() if fileInfo.Size() > 0 { document := tu.Document( tu.ID(chatId), tu.File(file), ) _, err = bot.SendDocument(context.Background(), document) if err != nil { logger.Error("Error in uploading IPLimitBannedPrevLog: ", err) } } else { logger.Warning("IPLimitBannedPrevLog file is empty, not uploading.") } file.Close() } else { logger.Error("Error in opening IPLimitBannedPrevLog file for backup: ", err) } file, err = os.Open(xray.GetIPLimitBannedLogPath()) if err == nil { // Check if the file is non-empty before attempting to upload fileInfo, _ := file.Stat() if fileInfo.Size() > 0 { document := tu.Document( tu.ID(chatId), tu.File(file), ) _, err = bot.SendDocument(context.Background(), document) if err != nil { logger.Error("Error in uploading IPLimitBannedLog: ", err) } } else { logger.Warning("IPLimitBannedLog file is empty, not uploading.") } file.Close() } else { logger.Error("Error in opening IPLimitBannedLog file for backup: ", err) } } // sendCallbackAnswerTgBot answers a callback query with a message. func (t *Tgbot) sendCallbackAnswerTgBot(id string, message string) { params := telego.AnswerCallbackQueryParams{ CallbackQueryID: id, Text: message, } if err := bot.AnswerCallbackQuery(context.Background(), ¶ms); err != nil { logger.Warning(err) } } // editMessageCallbackTgBot edits the reply markup of a message. func (t *Tgbot) editMessageCallbackTgBot(chatId int64, messageID int, inlineKeyboard *telego.InlineKeyboardMarkup) { params := telego.EditMessageReplyMarkupParams{ ChatID: tu.ID(chatId), MessageID: messageID, ReplyMarkup: inlineKeyboard, } if _, err := bot.EditMessageReplyMarkup(context.Background(), ¶ms); err != nil { logger.Warning(err) } } // editMessageTgBot edits the text and reply markup of a message. func (t *Tgbot) editMessageTgBot(chatId int64, messageID int, text string, inlineKeyboard ...*telego.InlineKeyboardMarkup) { params := telego.EditMessageTextParams{ ChatID: tu.ID(chatId), MessageID: messageID, Text: text, ParseMode: "HTML", } if len(inlineKeyboard) > 0 { params.ReplyMarkup = inlineKeyboard[0] } if _, err := bot.EditMessageText(context.Background(), ¶ms); err != nil { logger.Warning(err) } } // SendMsgToTgbotDeleteAfter sends a message and deletes it after a specified delay. func (t *Tgbot) SendMsgToTgbotDeleteAfter(chatId int64, msg string, delayInSeconds int, replyMarkup ...telego.ReplyMarkup) { // Determine if replyMarkup was passed; otherwise, set it to nil var replyMarkupParam telego.ReplyMarkup if len(replyMarkup) > 0 { replyMarkupParam = replyMarkup[0] // Use the first element } // Send the message sentMsg, err := bot.SendMessage(context.Background(), &telego.SendMessageParams{ ChatID: tu.ID(chatId), Text: msg, ReplyMarkup: replyMarkupParam, // Use the correct replyMarkup value }) if err != nil { logger.Warning("Failed to send message:", err) return } // Delete the sent message after the specified number of seconds go func() { time.Sleep(time.Duration(delayInSeconds) * time.Second) // Wait for the specified delay t.deleteMessageTgBot(chatId, sentMsg.MessageID) // Delete the message delete(userStates, chatId) }() } // deleteMessageTgBot deletes a message from the chat. func (t *Tgbot) deleteMessageTgBot(chatId int64, messageID int) { params := telego.DeleteMessageParams{ ChatID: tu.ID(chatId), MessageID: messageID, } if err := bot.DeleteMessage(context.Background(), ¶ms); err != nil { logger.Warning("Failed to delete message:", err) } else { logger.Info("Message deleted successfully") } } // isSingleWord checks if the text contains only a single word. func (t *Tgbot) isSingleWord(text string) bool { text = strings.TrimSpace(text) re := regexp.MustCompile(`\s+`) return re.MatchString(text) } ================================================ FILE: web/service/user.go ================================================ package service import ( "errors" "github.com/mhsanaei/3x-ui/v2/database" "github.com/mhsanaei/3x-ui/v2/database/model" "github.com/mhsanaei/3x-ui/v2/logger" "github.com/mhsanaei/3x-ui/v2/util/crypto" ldaputil "github.com/mhsanaei/3x-ui/v2/util/ldap" "github.com/xlzd/gotp" "gorm.io/gorm" ) // UserService provides business logic for user management and authentication. // It handles user creation, login, password management, and 2FA operations. type UserService struct { settingService SettingService } // GetFirstUser retrieves the first user from the database. // This is typically used for initial setup or when there's only one admin user. func (s *UserService) GetFirstUser() (*model.User, error) { db := database.GetDB() user := &model.User{} err := db.Model(model.User{}). First(user). Error if err != nil { return nil, err } return user, nil } func (s *UserService) CheckUser(username string, password string, twoFactorCode string) (*model.User, error) { db := database.GetDB() user := &model.User{} err := db.Model(model.User{}). Where("username = ?", username). First(user). Error if err == gorm.ErrRecordNotFound { return nil, errors.New("invalid credentials") } else if err != nil { logger.Warning("check user err:", err) return nil, err } if !crypto.CheckPasswordHash(user.Password, password) { ldapEnabled, _ := s.settingService.GetLdapEnable() if !ldapEnabled { return nil, errors.New("invalid credentials") } host, _ := s.settingService.GetLdapHost() port, _ := s.settingService.GetLdapPort() useTLS, _ := s.settingService.GetLdapUseTLS() bindDN, _ := s.settingService.GetLdapBindDN() ldapPass, _ := s.settingService.GetLdapPassword() baseDN, _ := s.settingService.GetLdapBaseDN() userFilter, _ := s.settingService.GetLdapUserFilter() userAttr, _ := s.settingService.GetLdapUserAttr() cfg := ldaputil.Config{ Host: host, Port: port, UseTLS: useTLS, BindDN: bindDN, Password: ldapPass, BaseDN: baseDN, UserFilter: userFilter, UserAttr: userAttr, } ok, err := ldaputil.AuthenticateUser(cfg, username, password) if err != nil || !ok { return nil, errors.New("invalid credentials") } } twoFactorEnable, err := s.settingService.GetTwoFactorEnable() if err != nil { logger.Warning("check two factor err:", err) return nil, err } if twoFactorEnable { twoFactorToken, err := s.settingService.GetTwoFactorToken() if err != nil { logger.Warning("check two factor token err:", err) return nil, err } if gotp.NewDefaultTOTP(twoFactorToken).Now() != twoFactorCode { return nil, errors.New("invalid 2fa code") } } return user, nil } func (s *UserService) UpdateUser(id int, username string, password string) error { db := database.GetDB() hashedPassword, err := crypto.HashPasswordAsBcrypt(password) if err != nil { return err } twoFactorEnable, err := s.settingService.GetTwoFactorEnable() if err != nil { return err } if twoFactorEnable { s.settingService.SetTwoFactorEnable(false) s.settingService.SetTwoFactorToken("") } return db.Model(model.User{}). Where("id = ?", id). Updates(map[string]any{"username": username, "password": hashedPassword}). Error } func (s *UserService) UpdateFirstUser(username string, password string) error { if username == "" { return errors.New("username can not be empty") } else if password == "" { return errors.New("password can not be empty") } hashedPassword, er := crypto.HashPasswordAsBcrypt(password) if er != nil { return er } db := database.GetDB() user := &model.User{} err := db.Model(model.User{}).First(user).Error if database.IsNotFound(err) { user.Username = username user.Password = hashedPassword return db.Model(model.User{}).Create(user).Error } else if err != nil { return err } user.Username = username user.Password = hashedPassword return db.Save(user).Error } ================================================ FILE: web/service/warp.go ================================================ package service import ( "bytes" "encoding/json" "fmt" "net/http" "os" "time" "github.com/mhsanaei/3x-ui/v2/logger" "github.com/mhsanaei/3x-ui/v2/util/common" ) // WarpService provides business logic for Cloudflare WARP integration. // It manages WARP configuration and connectivity settings. type WarpService struct { SettingService } func (s *WarpService) GetWarpData() (string, error) { warp, err := s.SettingService.GetWarp() if err != nil { return "", err } return warp, nil } func (s *WarpService) DelWarpData() error { err := s.SettingService.SetWarp("") if err != nil { return err } return nil } func (s *WarpService) GetWarpConfig() (string, error) { var warpData map[string]string warp, err := s.SettingService.GetWarp() if err != nil { return "", err } err = json.Unmarshal([]byte(warp), &warpData) if err != nil { return "", err } url := fmt.Sprintf("https://api.cloudflareclient.com/v0a2158/reg/%s", warpData["device_id"]) req, err := http.NewRequest("GET", url, nil) if err != nil { return "", err } req.Header.Set("Authorization", "Bearer "+warpData["access_token"]) client := &http.Client{} resp, err := client.Do(req) if err != nil { return "", err } defer resp.Body.Close() buffer := &bytes.Buffer{} _, err = buffer.ReadFrom(resp.Body) if err != nil { return "", err } return buffer.String(), nil } func (s *WarpService) RegWarp(secretKey string, publicKey string) (string, error) { tos := time.Now().UTC().Format("2006-01-02T15:04:05.000Z") hostName, _ := os.Hostname() data := fmt.Sprintf(`{"key":"%s","tos":"%s","type": "PC","model": "x-ui", "name": "%s"}`, publicKey, tos, hostName) url := "https://api.cloudflareclient.com/v0a2158/reg" req, err := http.NewRequest("POST", url, bytes.NewBuffer([]byte(data))) if err != nil { return "", err } req.Header.Add("CF-Client-Version", "a-7.21-0721") req.Header.Add("Content-Type", "application/json") client := &http.Client{} resp, err := client.Do(req) if err != nil { return "", err } defer resp.Body.Close() buffer := &bytes.Buffer{} _, err = buffer.ReadFrom(resp.Body) if err != nil { return "", err } var rspData map[string]any err = json.Unmarshal(buffer.Bytes(), &rspData) if err != nil { return "", err } deviceId := rspData["id"].(string) token := rspData["token"].(string) license, ok := rspData["account"].(map[string]any)["license"].(string) if !ok { logger.Debug("Error accessing license value.") return "", err } warpData := fmt.Sprintf("{\n \"access_token\": \"%s\",\n \"device_id\": \"%s\",", token, deviceId) warpData += fmt.Sprintf("\n \"license_key\": \"%s\",\n \"private_key\": \"%s\"\n}", license, secretKey) s.SettingService.SetWarp(warpData) result := fmt.Sprintf("{\n \"data\": %s,\n \"config\": %s\n}", warpData, buffer.String()) return result, nil } func (s *WarpService) SetWarpLicense(license string) (string, error) { var warpData map[string]string warp, err := s.SettingService.GetWarp() if err != nil { return "", err } err = json.Unmarshal([]byte(warp), &warpData) if err != nil { return "", err } url := fmt.Sprintf("https://api.cloudflareclient.com/v0a2158/reg/%s/account", warpData["device_id"]) data := fmt.Sprintf(`{"license": "%s"}`, license) req, err := http.NewRequest("PUT", url, bytes.NewBuffer([]byte(data))) if err != nil { return "", err } req.Header.Set("Authorization", "Bearer "+warpData["access_token"]) client := &http.Client{} resp, err := client.Do(req) if err != nil { return "", err } defer resp.Body.Close() buffer := &bytes.Buffer{} _, err = buffer.ReadFrom(resp.Body) if err != nil { return "", err } var response map[string]any err = json.Unmarshal(buffer.Bytes(), &response) if err != nil { return "", err } if response["success"] == false { errorArr, _ := response["errors"].([]any) errorObj := errorArr[0].(map[string]any) return "", common.NewError(errorObj["code"], errorObj["message"]) } warpData["license_key"] = license newWarpData, err := json.MarshalIndent(warpData, "", " ") if err != nil { return "", err } s.SettingService.SetWarp(string(newWarpData)) return string(newWarpData), nil } ================================================ FILE: web/service/xray.go ================================================ package service import ( "encoding/json" "errors" "runtime" "sync" "github.com/mhsanaei/3x-ui/v2/logger" "github.com/mhsanaei/3x-ui/v2/xray" "go.uber.org/atomic" ) var ( p *xray.Process lock sync.Mutex isNeedXrayRestart atomic.Bool // Indicates that restart was requested for Xray isManuallyStopped atomic.Bool // Indicates that Xray was stopped manually from the panel result string ) // XrayService provides business logic for Xray process management. // It handles starting, stopping, restarting Xray, and managing its configuration. type XrayService struct { inboundService InboundService settingService SettingService xrayAPI xray.XrayAPI } // IsXrayRunning checks if the Xray process is currently running. func (s *XrayService) IsXrayRunning() bool { return p != nil && p.IsRunning() } // GetXrayErr returns the error from the Xray process, if any. func (s *XrayService) GetXrayErr() error { if p == nil { return nil } err := p.GetErr() if err == nil { return nil } if runtime.GOOS == "windows" && err.Error() == "exit status 1" { // exit status 1 on Windows means that Xray process was killed // as we kill process to stop in on Windows, this is not an error return nil } return err } // GetXrayResult returns the result string from the Xray process. func (s *XrayService) GetXrayResult() string { if result != "" { return result } if s.IsXrayRunning() { return "" } if p == nil { return "" } result = p.GetResult() if runtime.GOOS == "windows" && result == "exit status 1" { // exit status 1 on Windows means that Xray process was killed // as we kill process to stop in on Windows, this is not an error return "" } return result } // GetXrayVersion returns the version of the running Xray process. func (s *XrayService) GetXrayVersion() string { if p == nil { return "Unknown" } return p.GetVersion() } // RemoveIndex removes an element at the specified index from a slice. // Returns a new slice with the element removed. func RemoveIndex(s []any, index int) []any { return append(s[:index], s[index+1:]...) } // GetXrayConfig retrieves and builds the Xray configuration from settings and inbounds. func (s *XrayService) GetXrayConfig() (*xray.Config, error) { templateConfig, err := s.settingService.GetXrayConfigTemplate() if err != nil { return nil, err } xrayConfig := &xray.Config{} err = json.Unmarshal([]byte(templateConfig), xrayConfig) if err != nil { return nil, err } s.inboundService.AddTraffic(nil, nil) inbounds, err := s.inboundService.GetAllInbounds() if err != nil { return nil, err } for _, inbound := range inbounds { if !inbound.Enable { continue } // get settings clients settings := map[string]any{} json.Unmarshal([]byte(inbound.Settings), &settings) clients, ok := settings["clients"].([]any) if ok { // check users active or not clientStats := inbound.ClientStats for _, clientTraffic := range clientStats { indexDecrease := 0 for index, client := range clients { c := client.(map[string]any) if c["email"] == clientTraffic.Email { if !clientTraffic.Enable { clients = RemoveIndex(clients, index-indexDecrease) indexDecrease++ logger.Infof("Remove Inbound User %s due to expiration or traffic limit", c["email"]) } } } } // clear client config for additional parameters var final_clients []any for _, client := range clients { c := client.(map[string]any) if c["enable"] != nil { if enable, ok := c["enable"].(bool); ok && !enable { continue } } for key := range c { if key != "email" && key != "id" && key != "password" && key != "flow" && key != "method" { delete(c, key) } if c["flow"] == "xtls-rprx-vision-udp443" { c["flow"] = "xtls-rprx-vision" } } final_clients = append(final_clients, any(c)) } settings["clients"] = final_clients modifiedSettings, err := json.MarshalIndent(settings, "", " ") if err != nil { return nil, err } inbound.Settings = string(modifiedSettings) } if len(inbound.StreamSettings) > 0 { // Unmarshal stream JSON var stream map[string]any json.Unmarshal([]byte(inbound.StreamSettings), &stream) // Remove the "settings" field under "tlsSettings" and "realitySettings" tlsSettings, ok1 := stream["tlsSettings"].(map[string]any) realitySettings, ok2 := stream["realitySettings"].(map[string]any) if ok1 || ok2 { if ok1 { delete(tlsSettings, "settings") } else if ok2 { delete(realitySettings, "settings") } } delete(stream, "externalProxy") newStream, err := json.MarshalIndent(stream, "", " ") if err != nil { return nil, err } inbound.StreamSettings = string(newStream) } inboundConfig := inbound.GenXrayInboundConfig() xrayConfig.InboundConfigs = append(xrayConfig.InboundConfigs, *inboundConfig) } return xrayConfig, nil } // GetXrayTraffic fetches the current traffic statistics from the running Xray process. func (s *XrayService) GetXrayTraffic() ([]*xray.Traffic, []*xray.ClientTraffic, error) { if !s.IsXrayRunning() { err := errors.New("xray is not running") logger.Debug("Attempted to fetch Xray traffic, but Xray is not running:", err) return nil, nil, err } apiPort := p.GetAPIPort() s.xrayAPI.Init(apiPort) defer s.xrayAPI.Close() traffic, clientTraffic, err := s.xrayAPI.GetTraffic(true) if err != nil { logger.Debug("Failed to fetch Xray traffic:", err) return nil, nil, err } return traffic, clientTraffic, nil } // RestartXray restarts the Xray process, optionally forcing a restart even if config unchanged. func (s *XrayService) RestartXray(isForce bool) error { lock.Lock() defer lock.Unlock() logger.Debug("restart Xray, force:", isForce) isManuallyStopped.Store(false) xrayConfig, err := s.GetXrayConfig() if err != nil { return err } if s.IsXrayRunning() { if !isForce && p.GetConfig().Equals(xrayConfig) && !isNeedXrayRestart.Load() { logger.Debug("It does not need to restart Xray") return nil } p.Stop() } p = xray.NewProcess(xrayConfig) result = "" err = p.Start() if err != nil { return err } return nil } // StopXray stops the running Xray process. func (s *XrayService) StopXray() error { lock.Lock() defer lock.Unlock() isManuallyStopped.Store(true) logger.Debug("Attempting to stop Xray...") if s.IsXrayRunning() { return p.Stop() } return errors.New("xray is not running") } // SetToNeedRestart marks that Xray needs to be restarted. func (s *XrayService) SetToNeedRestart() { isNeedXrayRestart.Store(true) } // IsNeedRestartAndSetFalse checks if restart is needed and resets the flag to false. func (s *XrayService) IsNeedRestartAndSetFalse() bool { return isNeedXrayRestart.CompareAndSwap(true, false) } // DidXrayCrash checks if Xray crashed by verifying it's not running and wasn't manually stopped. func (s *XrayService) DidXrayCrash() bool { return !s.IsXrayRunning() && !isManuallyStopped.Load() } ================================================ FILE: web/service/xray_setting.go ================================================ package service import ( _ "embed" "encoding/json" "github.com/mhsanaei/3x-ui/v2/util/common" "github.com/mhsanaei/3x-ui/v2/xray" ) // XraySettingService provides business logic for Xray configuration management. // It handles validation and storage of Xray template configurations. type XraySettingService struct { SettingService } func (s *XraySettingService) SaveXraySetting(newXraySettings string) error { if err := s.CheckXrayConfig(newXraySettings); err != nil { return err } return s.SettingService.saveSetting("xrayTemplateConfig", newXraySettings) } func (s *XraySettingService) CheckXrayConfig(XrayTemplateConfig string) error { xrayConfig := &xray.Config{} err := json.Unmarshal([]byte(XrayTemplateConfig), xrayConfig) if err != nil { return common.NewError("xray template config invalid:", err) } return nil } ================================================ FILE: web/session/session.go ================================================ // Package session provides session management utilities for the 3x-ui web panel. // It handles user authentication state, login sessions, and session storage using Gin sessions. package session import ( "encoding/gob" "net/http" "github.com/mhsanaei/3x-ui/v2/database/model" "github.com/gin-contrib/sessions" "github.com/gin-gonic/gin" ) const ( loginUserKey = "LOGIN_USER" defaultPath = "/" ) func init() { gob.Register(model.User{}) } // SetLoginUser stores the authenticated user in the session. // The user object is serialized and stored for subsequent requests. func SetLoginUser(c *gin.Context, user *model.User) { if user == nil { return } s := sessions.Default(c) s.Set(loginUserKey, *user) } // SetMaxAge configures the session cookie maximum age in seconds. // This controls how long the session remains valid before requiring re-authentication. func SetMaxAge(c *gin.Context, maxAge int) { s := sessions.Default(c) s.Options(sessions.Options{ Path: defaultPath, MaxAge: maxAge, HttpOnly: true, SameSite: http.SameSiteLaxMode, }) } // GetLoginUser retrieves the authenticated user from the session. // Returns nil if no user is logged in or if the session data is invalid. func GetLoginUser(c *gin.Context) *model.User { s := sessions.Default(c) obj := s.Get(loginUserKey) if obj == nil { return nil } user, ok := obj.(model.User) if !ok { s.Delete(loginUserKey) return nil } return &user } // IsLogin checks if a user is currently authenticated in the session. // Returns true if a valid user session exists, false otherwise. func IsLogin(c *gin.Context) bool { return GetLoginUser(c) != nil } // ClearSession removes all session data and invalidates the session. // This effectively logs out the user and clears any stored session information. func ClearSession(c *gin.Context) { s := sessions.Default(c) s.Clear() s.Options(sessions.Options{ Path: defaultPath, MaxAge: -1, HttpOnly: true, SameSite: http.SameSiteLaxMode, }) } ================================================ FILE: web/translation/translate.ar_EG.toml ================================================ "username" = "اسم المستخدم" "password" = "الباسورد" "login" = "تسجيل الدخول" "confirm" = "تأكيد" "cancel" = "إلغاء" "close" = "إغلاق" "create" = "إنشاء" "update" = "تحديث" "copy" = "نسخ" "copied" = "اتنسخ" "download" = "تحميل" "remark" = "ملاحظة" "enable" = "مفعل" "protocol" = "بروتوكول" "search" = "بحث" "filter" = "فلترة" "loading" = "جاري التحميل..." "second" = "ثانية" "minute" = "دقيقة" "hour" = "ساعة" "day" = "يوم" "check" = "شيك" "indefinite" = "غير محدد" "unlimited" = "غير محدود" "none" = "مفيش" "qrCode" = "كود QR" "info" = "معلومات أكتر" "edit" = "تعديل" "delete" = "مسح" "reset" = "إعادة ضبط" "noData" = "لا توجد بيانات." "copySuccess" = "اتنسخ بنجاح" "sure" = "متأكد؟" "encryption" = "تشفير" "useIPv4ForHost" = "استخدم IPv4 للمضيف" "transmission" = "نقل" "host" = "المستضيف" "path" = "مسار" "camouflage" = "تمويه" "status" = "الحالة" "enabled" = "مفعل" "disabled" = "معطل" "depleted" = "خلص" "depletingSoon" = "هينتهي قريب" "offline" = "أوفلاين" "online" = "أونلاين" "domainName" = "اسم الدومين" "monitor" = "المسمع IP" "certificate" = "شهادة رقمية" "fail" = "فشل" "comment" = "تعليق" "success" = "تم بنجاح" "lastOnline" = "آخر متصل" "getVersion" = "جيب النسخة" "install" = "تثبيت" "clients" = "عملاء" "usage" = "استخدام" "twoFactorCode" = "الكود" "remained" = "المتبقي" "security" = "أمان" "secAlertTitle" = "تنبيه أمني" "secAlertSsl" = "الاتصال ده مش آمن. ابعد عن إدخال معلومات حساسة لغاية ما تشغل TLS لحماية البيانات." "secAlertConf" = "بعض الإعدادات معرضة لهجمات. ينصح بتعزيز بروتوكولات الأمان عشان تمنع الاختراقات المحتملة." "secAlertSSL" = "البانل مش مؤمن. حمّل شهادة TLS لحماية البيانات." "secAlertPanelPort" = "بورت البانل الافتراضي معرض للخطر. ياريت تغير لبورت عشوائي أو محدد." "secAlertPanelURI" = "مسار URI الافتراضي للبانل مش آمن. ياريت تضبط مسار URI معقد." "secAlertSubURI" = "مسار URI الافتراضي للاشتراك مش آمن. ياريت تضبط مسار URI معقد." "secAlertSubJsonURI" = "مسار URI الافتراضي لاشتراك JSON مش آمن. ياريت تضبط مسار URI معقد." "emptyDnsDesc" = "مفيش سيرفر DNS مضاف." "emptyFakeDnsDesc" = "مفيش سيرفر Fake DNS مضاف." "emptyBalancersDesc" = "مفيش موازن تحميل مضاف." "emptyReverseDesc" = "مفيش بروكسي عكسي مضاف." "somethingWentWrong" = "حدث خطأ ما" [subscription] "title" = "معلومات الاشتراك" "subId" = "معرّف الاشتراك" "status" = "الحالة" "downloaded" = "التنزيل" "uploaded" = "الرفع" "expiry" = "تاريخ الانتهاء" "totalQuota" = "الحصة الإجمالية" "individualLinks" = "روابط فردية" "active" = "نشط" "inactive" = "غير نشط" "unlimited" = "غير محدود" "noExpiry" = "بدون انتهاء" [menu] "theme" = "الثيم" "dark" = "داكن" "ultraDark" = "داكن جدًا" "dashboard" = "نظرة عامة" "inbounds" = "الإدخالات" "settings" = "إعدادات البانل" "xray" = "إعدادات Xray" "logout" = "تسجيل خروج" "link" = "إدارة" [pages.login] "hello" = "أهلا" "title" = "أهلاً وسهلاً" "loginAgain" = "انتهت صلاحية الجلسة، سجل دخول تاني" [pages.login.toasts] "invalidFormData" = "تنسيق البيانات المدخلة مش صحيح." "emptyUsername" = "اسم المستخدم مطلوب" "emptyPassword" = "الباسورد مطلوب" "wrongUsernameOrPassword" = "اسم المستخدم أو كلمة المرور أو كود المصادقة الثنائية غير صحيح." "successLogin" = "لقد تم تسجيل الدخول إلى حسابك بنجاح." [pages.index] "title" = "نظرة عامة" "cpu" = "المعالج" "logicalProcessors" = "المعالجات المنطقية" "frequency" = "التردد" "swap" = "Swap" "storage" = "تخزين" "memory" = "رام" "threads" = "خيوط المعالجة" "xrayStatus" = "Xray" "stopXray" = "إيقاف" "restartXray" = "إعادة تشغيل" "xraySwitch" = "النسخة" "xraySwitchClick" = "اختار النسخة اللي عايز تتحول لها." "xraySwitchClickDesk" = "اختار بحذر، النسخ القديمة ممكن ما تتوافقش مع الإعدادات الحالية." "xrayStatusUnknown" = "مش معروف" "xrayStatusRunning" = "شغالة" "xrayStatusStop" = "متوقفة" "xrayStatusError" = "فيها غلطة" "xrayErrorPopoverTitle" = "حصل خطأ أثناء تشغيل Xray" "operationHours" = "مدة التشغيل" "systemLoad" = "تحميل النظام" "systemLoadDesc" = "متوسط تحميل النظام في الدقائق 1, 5, و15" "connectionCount" = "إحصائيات الاتصال" "ipAddresses" = "عناوين IP" "toggleIpVisibility" = "بدل إظهار IP" "overallSpeed" = "السرعة الكلية" "upload" = "رفع" "download" = "تنزيل" "totalData" = "إجمالي البيانات" "sent" = "مرسل" "received" = "مستقبل" "documentation" = "التوثيق" "xraySwitchVersionDialog" = "هل تريد حقًا تغيير إصدار Xray؟" "xraySwitchVersionDialogDesc" = "سيؤدي هذا إلى تغيير إصدار Xray إلى #version#." "xraySwitchVersionPopover" = "تم تحديث Xray بنجاح" "geofileUpdateDialog" = "هل تريد حقًا تحديث ملف الجغرافيا؟" "geofileUpdateDialogDesc" = "سيؤدي هذا إلى تحديث ملف #filename#." "geofilesUpdateDialogDesc" = "سيؤدي هذا إلى تحديث كافة الملفات." "geofilesUpdateAll" = "تحديث الكل" "geofileUpdatePopover" = "تم تحديث ملف الجغرافيا بنجاح" "dontRefresh" = "التثبيت شغال، متعملش Refresh للصفحة" "logs" = "السجلات" "config" = "الإعدادات" "backup" = "نسخة احتياطية" "backupTitle" = "نسخة احتياطية واسترجاع قاعدة البيانات" "exportDatabase" = "اخزن نسخة" "exportDatabaseDesc" = "اضغط عشان تحمل ملف .db يحتوي على نسخة احتياطية لقاعدة البيانات الحالية على جهازك." "importDatabase" = "استرجاع" "importDatabaseDesc" = "اضغط عشان تختار وتحمل ملف .db من جهازك لاسترجاع قاعدة البيانات من نسخة احتياطية." "importDatabaseSuccess" = "تم استيراد قاعدة البيانات بنجاح" "importDatabaseError" = "حدث خطأ أثناء استيراد قاعدة البيانات" "readDatabaseError" = "حدث خطأ أثناء قراءة قاعدة البيانات" "getDatabaseError" = "حدث خطأ أثناء استرجاع قاعدة البيانات" "getConfigError" = "حدث خطأ أثناء استرجاع ملف الإعدادات" [pages.inbounds] "allTimeTraffic" = "إجمالي حركة المرور" "allTimeTrafficUsage" = "إجمالي الاستخدام طوال الوقت" "title" = "الإدخالات" "totalDownUp" = "إجمالي المرسل/المستقبل" "totalUsage" = "إجمالي الاستخدام" "inboundCount" = "عدد الإدخالات" "operate" = "القائمة" "enable" = "مفعل" "remark" = "ملاحظة" "protocol" = "بروتوكول" "port" = "بورت" "portMap" = "خريطة البورت" "traffic" = "الترافيك" "details" = "تفاصيل" "transportConfig" = "نقل" "expireDate" = "المدة" "createdAt" = "تاريخ الإنشاء" "updatedAt" = "تاريخ التحديث" "resetTraffic" = "إعادة ضبط الترافيك" "addInbound" = "أضف إدخال" "generalActions" = "إجراءات عامة" "autoRefresh" = "تحديث تلقائي" "autoRefreshInterval" = "الفاصل" "modifyInbound" = "تعديل الإدخال" "deleteInbound" = "حذف الإدخال" "deleteInboundContent" = "متأكد إنك عايز تحذف الإدخال؟" "deleteClient" = "حذف العميل" "deleteClientContent" = "متأكد إنك عايز تحذف العميل؟" "resetTrafficContent" = "متأكد إنك عايز تعيد ضبط الترافيك؟" "copyLink" = "انسخ الرابط" "address" = "العنوان" "network" = "الشبكة" "destinationPort" = "بورت الوجهة" "targetAddress" = "عنوان الهدف" "monitorDesc" = "سيبها فاضية لو عايز تستمع على كل الـ IPs" "meansNoLimit" = "= غير محدود. (الوحدة: جيجابايت)" "totalFlow" = "إجمالي التدفق" "leaveBlankToNeverExpire" = "سيبها فاضية عشان ماتنتهيش" "noRecommendKeepDefault" = "ننصح باستخدام الافتراضي" "certificatePath" = "مسار الملف" "certificateContent" = "محتوى الملف" "publicKey" = "المفتاح العام" "privatekey" = "المفتاح الخاص" "clickOnQRcode" = "اضغط على كود QR للنسخ" "client" = "عميل" "export" = "تصدير كل الروابط" "clone" = "استنساخ" "cloneInbound" = "استنساخ الإدخال" "cloneInboundContent" = "كل إعدادات الإدخال ده، غير البورت، IP الاستماع، والعملاء، هتتطبق على الاستنساخ." "cloneInboundOk" = "استنساخ" "resetAllTraffic" = "إعادة ضبط ترافيك كل الإدخالات" "resetAllTrafficTitle" = "إعادة ضبط ترافيك كل الإدخالات" "resetAllTrafficContent" = "متأكد إنك عايز تعيد ضبط الترافيك لكل الإدخالات؟" "resetInboundClientTraffics" = "إعادة ضبط ترافيك العملاء" "resetInboundClientTrafficTitle" = "إعادة ضبط ترافيك العملاء" "resetInboundClientTrafficContent" = "متأكد إنك عايز تعيد ضبط ترافيك عملاء الإدخال ده؟" "resetAllClientTraffics" = "إعادة ضبط ترافيك كل العملاء" "resetAllClientTrafficTitle" = "إعادة ضبط ترافيك كل العملاء" "resetAllClientTrafficContent" = "متأكد إنك عايز تعيد ضبط ترافيك كل العملاء؟" "delDepletedClients" = "حذف العملاء اللي خلصت" "delDepletedClientsTitle" = "حذف العملاء اللي خلصت" "delDepletedClientsContent" = "متأكد إنك عايز تحذف كل العملاء اللي خلصت؟" "email" = "الإيميل" "emailDesc" = "ادخل إيميل فريد." "IPLimit" = "تحديد IP" "IPLimitDesc" = "بيعطل الإدخال لو العدد زاد عن القيمة المحددة. (0 = تعطيل)" "IPLimitlog" = "سجل IP" "IPLimitlogDesc" = "سجل تاريخ الـ IPs. (عشان تفعل الإدخال بعد التعطيل، امسح السجل)" "IPLimitlogclear" = "امسح السجل" "setDefaultCert" = "استخدم شهادة البانل" "telegramDesc" = "ادخل ID شات Telegram. (استخدم '/id' في البوت) أو (@userinfobot)" "subscriptionDesc" = "عشان تلاقي رابط الاشتراك، ادخل على 'التفاصيل'. وكمان ممكن تستخدم نفس الاسم لعدة عملاء." "info" = "معلومات" "same" = "نفسه" "inboundData" = "بيانات الإدخال" "exportInbound" = "تصدير الإدخال" "import" = "استيراد" "importInbound" = "استيراد إدخال" "periodicTrafficResetTitle" = "إعادة تعيين حركة المرور" "periodicTrafficResetDesc" = "إعادة تعيين عداد حركة المرور تلقائيًا في فترات محددة" "lastReset" = "آخر إعادة تعيين" [pages.client] "add" = "أضف عميل" "edit" = "تعديل عميل" "submitAdd" = "أضف العميل" "submitEdit" = "احفظ التعديلات" "clientCount" = "عدد العملاء" "bulk" = "إضافة بالجملة" "method" = "طريقة" "first" = "أول واحد" "last" = "آخر واحد" "prefix" = "بادئة" "postfix" = "لاحقة" "delayedStart" = "ابدأ بعد أول استخدام" "expireDays" = "المدة" "days" = "يوم/أيام" "renew" = "تجديد تلقائي" "renewDesc" = "تجديد تلقائي بعد انتهاء الصلاحية. (0 = تعطيل)(الوحدة: يوم)" [pages.inbounds.periodicTrafficReset] "never" = "أبداً" "daily" = "يومياً" "weekly" = "أسبوعياً" "monthly" = "شهرياً" [pages.inbounds.toasts] "obtain" = "تم الحصول عليه" "updateSuccess" = "تم التحديث بنجاح" "logCleanSuccess" = "تم مسح السجل" "inboundsUpdateSuccess" = "تم تحديث الواردات بنجاح" "inboundUpdateSuccess" = "تم تحديث الوارد بنجاح" "inboundCreateSuccess" = "تم إنشاء الوارد بنجاح" "inboundDeleteSuccess" = "تم حذف الوارد بنجاح" "inboundClientAddSuccess" = "تمت إضافة عميل(عملاء) وارد" "inboundClientDeleteSuccess" = "تم حذف عميل وارد" "inboundClientUpdateSuccess" = "تم تحديث عميل وارد" "delDepletedClientsSuccess" = "تم حذف جميع العملاء المستنفذين" "resetAllClientTrafficSuccess" = "تم إعادة تعيين كل حركة المرور من العميل" "resetAllTrafficSuccess" = "تم إعادة تعيين كل حركة المرور" "resetInboundClientTrafficSuccess" = "تم إعادة تعيين حركة المرور" "trafficGetError" = "خطأ في الحصول على حركات المرور" "getNewX25519CertError" = "حدث خطأ أثناء الحصول على شهادة X25519." "getNewmldsa65Error" = "حدث خطاء في الحصول على mldsa65." "getNewVlessEncError" = "حدث خطأ أثناء الحصول على VlessEnc." [pages.inbounds.stream.general] "request" = "طلب" "response" = "رد" "name" = "اسم" "value" = "قيمة" [pages.inbounds.stream.tcp] "version" = "نسخة" "method" = "طريقة" "path" = "مسار" "status" = "الحالة" "statusDescription" = "وصف الحالة" "requestHeader" = "رأس الطلب" "responseHeader" = "رأس الرد" [pages.settings] "title" = "إعدادات البانل" "save" = "حفظ" "infoDesc" = "كل تغيير هتعمله هنا لازم يتخزن. ياريت تعيد تشغيل البانل عشان التعديلات تتفعل." "restartPanel" = "إعادة تشغيل البانل" "restartPanelDesc" = "متأكد إنك عايز تعيد تشغيل البانل؟ لو ماقدرتش تدخل بعد إعادة التشغيل، شوف سجل البانل على السيرفر." "restartPanelSuccess" = "تم إعادة تشغيل اللوحة بنجاح" "actions" = "إجراءات" "resetDefaultConfig" = "استرجاع الافتراضي" "panelSettings" = "عام" "securitySettings" = "المصادقة" "TGBotSettings" = "بوت Telegram" "panelListeningIP" = "IP الاستماع" "panelListeningIPDesc" = "عنوان IP للبانل. (سيبه فاضي عشان يستمع على كل الـ IPs)" "panelListeningDomain" = "دومين الاستماع" "panelListeningDomainDesc" = "اسم الدومين للبانل. (سيبه فاضي عشان يستمع على كل الدومينات والـ IPs)" "panelPort" = "بورت الاستماع" "panelPortDesc" = "رقم البورت للبانل. (لازم يكون بورت فاضي)" "publicKeyPath" = "مسار المفتاح العام" "publicKeyPathDesc" = "مسار ملف المفتاح العام للبانل. (يبدأ بـ '/')" "privateKeyPath" = "مسار المفتاح الخاص" "privateKeyPathDesc" = "مسار ملف المفتاح الخاص للبانل. (يبدأ بـ '/')" "panelUrlPath" = "مسار URI" "panelUrlPathDesc" = "مسار URI للبانل. (يبدأ بـ '/' وبينتهي بـ '/')" "pageSize" = "حجم الصفحة" "pageSizeDesc" = "حدد حجم الصفحة لجدول الإدخالات. (0 = تعطيل)" "remarkModel" = "نموذج الملاحظة وحرف الفصل" "datepicker" = "نوع التقويم" "datepickerPlaceholder" = "اختار التاريخ" "datepickerDescription" = "المهام المجدولة هتشتغل بناءً على التقويم ده." "sampleRemark" = "مثال للملاحظة" "oldUsername" = "اسم المستخدم الحالي" "currentPassword" = "الباسورد الحالي" "newUsername" = "اسم المستخدم الجديد" "newPassword" = "الباسورد الجديد" "telegramBotEnable" = "تفعيل بوت Telegram" "telegramBotEnableDesc" = "يفعل بوت Telegram." "telegramToken" = "توكن Telegram" "telegramTokenDesc" = "توكن البوت اللي جبت من '@BotFather'." "telegramProxy" = "بروكسي SOCKS" "telegramProxyDesc" = "يفعل بروكسي SOCKS5 للاتصال بـ Telegram. (اضبط الإعدادات حسب الدليل)" "telegramAPIServer" = "سيرفر Telegram API" "telegramAPIServerDesc" = "سيرفر Telegram API المستخدم. سيبه فاضي لاستخدام الافتراضي." "telegramChatId" = "ID شات الأدمن" "telegramChatIdDesc" = "ID شات الأدمن في Telegram. (مفصول بفواصل)(تقدر تجيبه من @userinfobot) أو (استخدم '/id' في البوت)" "telegramNotifyTime" = "وقت الإشعار" "telegramNotifyTimeDesc" = "وقت إشعار البوت للتقارير الدورية. (استخدم صيغة وقت crontab)" "tgNotifyBackup" = "نسخة احتياطية لقاعدة البيانات" "tgNotifyBackupDesc" = "ابعت ملف النسخة الاحتياطية لقاعدة البيانات مع التقرير." "tgNotifyLogin" = "إشعار بتسجيل الدخول" "tgNotifyLoginDesc" = "استقبل إشعار بكل محاولة تسجيل دخول للبانل مع اسم المستخدم، الـ IP، والوقت." "sessionMaxAge" = "مدة الجلسة" "sessionMaxAgeDesc" = "المدة اللي تفضل فيها مسجل دخول. (الوحدة: دقيقة)" "expireTimeDiff" = "تنبيه بتاريخ الانتهاء" "expireTimeDiffDesc" = "استقبل تنبيه قبل ما توصل لتاريخ الانتهاء بالمدة المحددة. (الوحدة: يوم)" "trafficDiff" = "تنبيه حد الترافيك" "trafficDiffDesc" = "استقبل تنبيه عند وصول الترافيك للحد المحدد. (الوحدة: جيجابايت)" "tgNotifyCpu" = "تنبيه حمل المعالج" "tgNotifyCpuDesc" = "استقبل تنبيه لو حمل المعالج عدى الحد المحدد. (الوحدة: %)" "timeZone" = "المنطقة الزمنية" "timeZoneDesc" = "المهام المجدولة هتشتغل بناءً على المنطقة الزمنية دي." "subSettings" = "الاشتراك" "subEnable" = "تفعيل خدمة الاشتراك" "subEnableDesc" = "يفعل خدمة الاشتراك." "subJsonEnable" = "تمكين/تعطيل نقطة نهاية اشتراك JSON بشكل مستقل." "subTitle" = "عنوان الاشتراك" "subTitleDesc" = "العنوان اللي هيظهر في عميل VPN" "subSupportUrl" = "رابط الدعم" "subSupportUrlDesc" = "رابط الدعم الفني المعروض في عميل VPN" "subProfileUrl" = "رابط الملف الشخصي" "subProfileUrlDesc" = "رابط لموقعك الإلكتروني يظهر في عميل VPN" "subAnnounce" = "إعلان" "subAnnounceDesc" = "نص الإعلان المعروض في عميل VPN" "subEnableRouting" = "تفعيل التوجيه" "subEnableRoutingDesc" = "إعداد عام لتمكين التوجيه (Routing) في عميل VPN. (فقط لـ Happ)" "subRoutingRules" = "قواعد التوجيه" "subRoutingRulesDesc" = "قواعد التوجيه العامة لعميل VPN. (فقط لـ Happ)" "subListen" = "IP الاستماع" "subListenDesc" = "عنوان IP لخدمة الاشتراك. (سيبه فاضي عشان يستمع على كل الـ IPs)" "subPort" = "بورت الاستماع" "subPortDesc" = "رقم البورت لخدمة الاشتراك. (لازم يكون بورت فاضي)" "subCertPath" = "مسار المفتاح العام" "subCertPathDesc" = "مسار ملف المفتاح العام لخدمة الاشتراك. (يبدأ بـ '/')" "subKeyPath" = "مسار المفتاح الخاص" "subKeyPathDesc" = "مسار ملف المفتاح الخاص لخدمة الاشتراك. (يبدأ بـ '/')" "subPath" = "مسار URI" "subPathDesc" = "مسار URI لخدمة الاشتراك. (يبدأ بـ '/' وبينتهي بـ '/')" "subDomain" = "دومين الاستماع" "subDomainDesc" = "اسم الدومين لخدمة الاشتراك. (سيبه فاضي عشان يستمع على كل الدومينات والـ IPs)" "subUpdates" = "فترات التحديث" "subUpdatesDesc" = "فترات تحديث رابط الاشتراك في تطبيقات العملاء. (الوحدة: ساعة)" "subEncrypt" = "تشفير" "subEncryptDesc" = "المحتوى اللي هيترجع من خدمة الاشتراك هيكون مشفر بـ Base64." "subShowInfo" = "اظهر معلومات الاستخدام" "subShowInfoDesc" = "هيظهر الترافيك المتبقي والتاريخ في تطبيقات العملاء." "subURI" = "مسار البروكسي العكسي" "subURIDesc" = "مسار URI لرابط الاشتراك عشان تستخدمه ورا البروكسي." "externalTrafficInformEnable" = "تنبيه الترافيك الخارجي" "externalTrafficInformEnableDesc" = "يبعت تنبيه لـ API خارجي مع كل تحديث للترافيك." "externalTrafficInformURI" = "مسار تنبيه الترافيك الخارجي" "externalTrafficInformURIDesc" = "تحديثات الترافيك هتتبعت للمسار ده." "fragment" = "تجزئة" "fragmentDesc" = "يفعل تجزئة لحزمة TLS hello." "fragmentSett" = "إعدادات التجزئة" "noisesDesc" = "يفعل التشويش." "noisesSett" = "إعدادات التشويش" "mux" = "MUX" "muxDesc" = "ينقل أكثر من تيار بيانات مستقل خلال تيار بيانات واحد قائم." "muxSett" = "إعدادات MUX" "direct" = "اتصال مباشر" "directDesc" = "ينشئ اتصال مباشر مع الدومينات أو نطاقات IP لدولة معينة." "notifications" = "الإشعارات" "certs" = "الشهادات" "externalTraffic" = "الترافيك الخارجي" "dateAndTime" = "التاريخ والوقت" "proxyAndServer" = "البروكسي والسيرفر" "intervals" = "الفترات" "information" = "المعلومات" "language" = "اللغة" "telegramBotLanguage" = "لغة بوت Telegram" [pages.xray] "title" = "إعدادات Xray" "save" = "احفظ" "restart" = "أعد تشغيل Xray" "restartSuccess" = "تم إعادة تشغيل Xray بنجاح" "stopSuccess" = "تم إيقاف Xray بنجاح" "restartError" = "حدث خطأ أثناء إعادة تشغيل Xray." "stopError" = "حدث خطأ أثناء إيقاف Xray." "basicTemplate" = "أساسي" "advancedTemplate" = "متقدم" "generalConfigs" = "إعدادات عامة" "generalConfigsDesc" = "الخيارات دي هتحدد التعديلات العامة." "logConfigs" = "السجلات" "logConfigsDesc" = "السجلات ممكن تأثر على كفاءة السيرفر. ننصح بتفعيلها بحكمة لما تكون محتاجها." "blockConfigsDesc" = "الخيارات دي هتحجب الترافيك بناءً على بروتوكولات ومواقع محددة." "basicRouting" = "توجيه أساسي" "blockConnectionsConfigsDesc" = "الخيارات دي هتحجب الترافيك بناءً على الدولة المطلوبة." "directConnectionsConfigsDesc" = "الاتصال المباشر بيضمن إن الترافيك المعين مايمرش من سيرفر تاني." "blockips" = "حظر IPs" "blockdomains" = "حظر دومينات" "directips" = "اتصالات مباشرة لـ IPs" "directdomains" = "اتصالات مباشرة للدومينات" "ipv4Routing" = "توجيه IPv4" "ipv4RoutingDesc" = "الخيارات دي هتوجه الترافيك بناءً على وجهة معينة عبر IPv4." "warpRouting" = "توجيه WARP" "warpRoutingDesc" = "الخيارات دي هتوجه الترافيك بناءً على وجهة معينة عبر WARP." "Template" = "قالب إعدادات Xray المتقدم" "TemplateDesc" = "ملف إعدادات Xray النهائي هيتولد بناءً على القالب ده." "FreedomStrategy" = "استراتيجية بروتوكول الحرية" "FreedomStrategyDesc" = "اختار استراتيجية المخرجات للشبكة في بروتوكول الحرية." "RoutingStrategy" = "استراتيجية التوجيه العامة" "RoutingStrategyDesc" = "حدد استراتيجية التوجيه الإجمالية لحل كل الطلبات." "outboundTestUrl" = "رابط اختبار المخرج" "outboundTestUrlDesc" = "الرابط المستخدم عند اختبار اتصال المخرج" "Torrent" = "حظر بروتوكول التورنت" "Inbounds" = "الإدخالات" "InboundsDesc" = "قبول العملاء المعينين." "Outbounds" = "المخرجات" "Balancers" = "موازنات التحميل" "OutboundsDesc" = "حدد مسار الترافيك الصادر." "Routings" = "قواعد التوجيه" "RoutingsDesc" = "أولوية كل قاعدة مهمة جداً!" "completeTemplate" = "الكل" "logLevel" = "مستوى السجلات" "logLevelDesc" = "مستوى السجل الخاص بالأخطاء، اللي بيوضح المعلومات المطلوبة للتسجيل." "accessLog" = "سجل الوصول" "accessLogDesc" = "مسار ملف سجل الوصول. القيمة الخاصة 'none' بتعطل سجل الوصول." "errorLog" = "سجل الأخطاء" "errorLogDesc" = "مسار ملف سجل الأخطاء. القيمة الخاصة 'none' بتعطل سجل الأخطاء." "dnsLog" = "سجل DNS" "dnsLogDesc" = "لو هتسجل استعلامات DNS." "maskAddress" = "إخفاء العنوان" "maskAddressDesc" = "إخفاء عنوان الـ IP؛ لو مفعل، هيستبدل تلقائياً عنوان IP اللي بيظهر في السجل." "statistics" = "إحصائيات" "statsInboundUplink" = "إحصائيات رفع الإدخال" "statsInboundUplinkDesc" = "تفعيل جمع الإحصائيات لترافيك الرفع لكل بروكسي من الإدخالات." "statsInboundDownlink" = "إحصائيات تنزيل الإدخال" "statsInboundDownlinkDesc" = "تفعيل جمع الإحصائيات لترافيك التنزيل لكل بروكسي من الإدخالات." "statsOutboundUplink" = "إحصائيات رفع المخرجات" "statsOutboundUplinkDesc" = "تفعيل جمع الإحصائيات لترافيك الرفع لكل بروكسي من المخرجات." "statsOutboundDownlink" = "إحصائيات تنزيل المخرجات" "statsOutboundDownlinkDesc" = "تفعيل جمع الإحصائيات لترافيك التنزيل لكل بروكسي من المخرجات." [pages.xray.rules] "first" = "أول" "last" = "آخر" "up" = "فوق" "down" = "تحت" "source" = "المصدر" "dest" = "الوجهة" "inbound" = "إدخال" "outbound" = "مخرج" "balancer" = "موازن" "info" = "معلومات" "add" = "أضف قاعدة" "edit" = "عدل القاعدة" "useComma" = "عناصر مفصولة بفواصل" [pages.xray.outbound] "addOutbound" = "أضف مخرج" "addReverse" = "أضف عكسي" "editOutbound" = "عدل المخرج" "editReverse" = "عدل العكسي" "tag" = "تاج" "tagDesc" = "تاج فريد" "address" = "العنوان" "reverse" = "عكسي" "domain" = "دومين" "type" = "النوع" "bridge" = "جسر" "portal" = "بوابة" "link" = "رابط" "intercon" = "تواصل" "settings" = "إعدادات" "accountInfo" = "معلومات الحساب" "outboundStatus" = "حالة المخرج" "sendThrough" = "أرسل من خلال" "test" = "اختبار" "testResult" = "نتيجة الاختبار" "testing" = "جاري اختبار الاتصال..." "testSuccess" = "الاختبار ناجح" "testFailed" = "فشل الاختبار" "testError" = "فشل اختبار المخرج" [pages.xray.balancer] "addBalancer" = "أضف موازن تحميل" "editBalancer" = "عدل موازن التحميل" "balancerStrategy" = "استراتيجية الموازن" "balancerSelectors" = "المحددات" "tag" = "تاج" "tagDesc" = "تاج فريد" "balancerDesc" = "ماينفعش تستخدم balancerTag و outboundTag مع بعض. لو اتستخدموا مع بعض، outboundTag هو اللي هيشتغل." [pages.xray.wireguard] "secretKey" = "المفتاح السري" "publicKey" = "المفتاح العام" "allowedIPs" = "عناوين IP المسموح بها" "endpoint" = "النهاية" "psk" = "المفتاح المشترك" "domainStrategy" = "استراتيجية الدومين" [pages.xray.tun] "nameDesc" = "اسم واجهة TUN. القيمة الافتراضية هي 'xray0'" "mtuDesc" = "وحدة النقل الأقصى. الحد الأقصى لحجم حزم البيانات. القيمة الافتراضية هي 1500" "userLevel" = "مستوى المستخدم" "userLevelDesc" = "ستستخدم جميع الاتصالات المُرسلة عبر هذا الإدخال مستوى المستخدم هذا. القيمة الافتراضية هي 0" [pages.xray.dns] "enable" = "فعل DNS" "enableDesc" = "فعل سيرفر DNS المدمج" "tag" = "تاج إدخال DNS" "tagDesc" = "التاج ده هيبقى متاح كإدخال في قواعد التوجيه." "clientIp" = "IP العميل" "clientIpDesc" = "بيحدد موقع العميل خلال استعلامات DNS" "disableCache" = "تعطيل الكاش" "disableCacheDesc" = "بيعطل تخزين نتائج DNS مؤقتاً" "disableFallback" = "تعطيل النسخ الاحتياطي" "disableFallbackDesc" = "بيعطل استعلامات DNS الاحتياطية" "disableFallbackIfMatch" = "تعطيل النسخ الاحتياطي عند التطابق" "disableFallbackIfMatchDesc" = "بيعطل استعلامات DNS الاحتياطية لما يتحقق تطابق مع قائمة الدومينات" "enableParallelQuery" = "تفعيل الاستعلام المتوازي" "enableParallelQueryDesc" = "تفعيل استعلامات DNS المتوازية لعدة خوادم لحل أسرع" "strategy" = "استراتيجية الاستعلام" "strategyDesc" = "الاستراتيجية العامة لحل أسماء الدومين" "add" = "أضف سيرفر" "edit" = "عدل السيرفر" "domains" = "الدومينات" "expectIPs" = "العناوين المتوقعة" "unexpectIPs" = "عناوين IP غير متوقعة" "useSystemHosts" = "استخدام ملف Hosts الخاص بالنظام" "useSystemHostsDesc" = "استخدام ملف hosts من نظام مثبت" "usePreset" = "استخدام النموذج" "dnsPresetTitle" = "قوالب DNS" "dnsPresetFamily" = "العائلي" [pages.xray.fakedns] "add" = "أضف Fake DNS" "edit" = "عدل Fake DNS" "ipPool" = "نطاق IP Pool" "poolSize" = "حجم المجموعة" [pages.settings.security] "admin" = "بيانات الأدمن" "twoFactor" = "المصادقة الثنائية" "twoFactorEnable" = "تفعيل المصادقة الثنائية" "twoFactorEnableDesc" = "يضيف طبقة إضافية من المصادقة لتعزيز الأمان." "twoFactorModalSetTitle" = "تفعيل المصادقة الثنائية" "twoFactorModalDeleteTitle" = "تعطيل المصادقة الثنائية" "twoFactorModalSteps" = "لإعداد المصادقة الثنائية، قم ببعض الخطوات:" "twoFactorModalFirstStep" = "1. امسح رمز QR هذا في تطبيق المصادقة أو انسخ الرمز الموجود بجانب رمز QR والصقه في التطبيق" "twoFactorModalSecondStep" = "2. أدخل الرمز من التطبيق" "twoFactorModalRemoveStep" = "أدخل الرمز من التطبيق لإزالة المصادقة الثنائية." "twoFactorModalChangeCredentialsTitle" = "تغيير بيانات الاعتماد" "twoFactorModalChangeCredentialsStep" = "أدخل الرمز من التطبيق لتغيير بيانات اعتماد المسؤول." "twoFactorModalSetSuccess" = "تم إنشاء المصادقة الثنائية بنجاح" "twoFactorModalDeleteSuccess" = "تم حذف المصادقة الثنائية بنجاح" "twoFactorModalError" = "رمز خاطئ" [pages.settings.toasts] "modifySettings" = "تم تغيير المعلمات." "getSettings" = "حدث خطأ أثناء استرداد المعلمات." "modifyUserError" = "حدث خطأ أثناء تغيير بيانات اعتماد المسؤول." "modifyUser" = "لقد قمت بتغيير بيانات اعتماد المسؤول بنجاح." "originalUserPassIncorrect" = "اسم المستخدم أو الباسورد الحالي غير صحيح" "userPassMustBeNotEmpty" = "اسم المستخدم والباسورد الجديدين فاضيين" "getOutboundTrafficError" = "خطأ في الحصول على حركات المرور الصادرة" "resetOutboundTrafficError" = "خطأ في إعادة تعيين حركات المرور الصادرة" [tgbot] "keyboardClosed" = "❌ لوحة المفاتيح مغلقة!" "noResult" = "❗ لا يوجد نتائج!" "noQuery" = "❌ لم يتم العثور على الاستعلام! يرجى استخدام الأمر مرة أخرى!" "wentWrong" = "❌ حدث خطأ ما!" "noIpRecord" = "❗ لا يوجد سجل IP!" "noInbounds" = "❗ لم يتم العثور على أي وارد!" "unlimited" = "♾ غير محدود (إعادة تعيين)" "add" = "إضافة" "month" = "شهر" "months" = "أشهر" "day" = "يوم" "days" = "أيام" "hours" = "ساعات" "minutes" = "دقائق" "unknown" = "غير معروف" "inbounds" = "الواردات" "clients" = "العملاء" "offline" = "🔴 غير متصل" "online" = "🟢 متصل" [tgbot.commands] "unknown" = "❗ أمر مش معروف." "pleaseChoose" = "👇 من فضلك اختار:\r\n" "help" = "🤖 أهلا بيك في البوت! البوت ده معمول عشان يديك بيانات معينة من البانل ويسمحلك بالتعديلات." "start" = "👋 أهلا {{ .Firstname }}.\r\n" "welcome" = "🤖 أهلا بيك في بوت إدارة {{ .Hostname }}.\r\n" "status" = "✅ البوت شغال!" "usage" = "❗ من فضلك ادخل نص للتبحث عنه!" "getID" = "🆔 الـ ID بتاعك: {{ .ID }}" "helpAdminCommands" = "عشان تعيد تشغيل Xray Core:\r\n/restart\r\n\r\nعشان تدور على إيميل عميل:\r\n/usage [Email]\r\n\r\nعشان تدور على إدخالات (مع إحصائيات العملاء):\r\n/inbound [Remark]\r\n\r\nID شات Telegram:\r\n/id" "helpClientCommands" = "عشان تدور على الإحصائيات، استخدم الأمر ده:\r\n\r\n/usage [Email]\r\n\r\nID شات Telegram:\r\n/id" "restartUsage" = "\r\n\r\n/restart" "restartSuccess" = "✅ العملية نجحت!" "restartFailed" = "❗ حصل خطأ في العملية.\r\n\r\nError: {{ .Error }}." "xrayNotRunning" = "❗ Xray Core مش شغال." "startDesc" = "عرض القائمة الرئيسية" "helpDesc" = "مساعدة البوت" "statusDesc" = "التحقق من حالة البوت" "idDesc" = "عرض معرف Telegram الخاص بك" [tgbot.messages] "cpuThreshold" = "🔴 حمل المعالج {{ .Percent }}% عدى الحد المسموح ({{ .Threshold }}%)" "selectUserFailed" = "❌ حصل خطأ في اختيار المستخدم!" "userSaved" = "✅ حفظت بيانات مستخدم Telegram." "loginSuccess" = "✅ تسجيل الدخول للبانل تم بنجاح.\r\n" "loginFailed" = "❗️فشل محاولة تسجيل الدخول للبانل.\r\n" "2faFailed" = "فشل 2FA" "report" = "🕰 التقارير المجدولة: {{ .RunTime }}\r\n" "datetime" = "⏰ التاريخ والوقت: {{ .DateTime }}\r\n" "hostname" = "💻 السيرفر: {{ .Hostname }}\r\n" "version" = "🚀 نسخة 3X-UI: {{ .Version }}\r\n" "xrayVersion" = "📡 نسخة Xray: {{ .XrayVersion }}\r\n" "ipv6" = "🌐 IPv6: {{ .IPv6 }}\r\n" "ipv4" = "🌐 IPv4: {{ .IPv4 }}\r\n" "ip" = "🌐 IP: {{ .IP }}\r\n" "ips" = "🔢 عناوين IP:\r\n{{ .IPs }}\r\n" "serverUpTime" = "⏳ وقت التشغيل: {{ .UpTime }} {{ .Unit }}\r\n" "serverLoad" = "📈 تحميل النظام: {{ .Load1 }}, {{ .Load2 }}, {{ .Load3 }}\r\n" "serverMemory" = "📋 الرام: {{ .Current }}/{{ .Total }}\r\n" "tcpCount" = "🔹 TCP: {{ .Count }}\r\n" "udpCount" = "🔸 UDP: {{ .Count }}\r\n" "traffic" = "🚦 الترافيك: {{ .Total }} (↑{{ .Upload }},↓{{ .Download }})\r\n" "xrayStatus" = "ℹ️ الحالة: {{ .State }}\r\n" "username" = "👤 اسم المستخدم: {{ .Username }}\r\n" "password" = "👤 الباسورد: {{ .Password }}\r\n" "time" = "⏰ الوقت: {{ .Time }}\r\n" "inbound" = "📍 الإدخال: {{ .Remark }}\r\n" "port" = "🔌 البورت: {{ .Port }}\r\n" "expire" = "📅 تاريخ الانتهاء: {{ .Time }}\r\n" "expireIn" = "📅 هيخلص بعد: {{ .Time }}\r\n" "active" = "💡 مفعل: {{ .Enable }}\r\n" "enabled" = "🚨 مفعل: {{ .Enable }}\r\n" "online" = "🌐 حالة الاتصال: {{ .Status }}\r\n" "lastOnline" = "🔙 آخر متصل: {{ .Time }}\r\n" "email" = "📧 الإيميل: {{ .Email }}\r\n" "upload" = "🔼 رفع: ↑{{ .Upload }}\r\n" "download" = "🔽 تنزيل: ↓{{ .Download }}\r\n" "total" = "📊 الإجمالي: ↑↓{{ .UpDown }} / {{ .Total }}\r\n" "TGUser" = "👤 مستخدم Telegram: {{ .TelegramID }}\r\n" "exhaustedMsg" = "🚨 نفذ {{ .Type }}:\r\n" "exhaustedCount" = "🚨 عدد النفاذ لـ {{ .Type }}:\r\n" "onlinesCount" = "🌐 العملاء الأونلاين: {{ .Count }}\r\n" "disabled" = "🛑 معطل: {{ .Disabled }}\r\n" "depleteSoon" = "🔜 هينتهي قريب: {{ .Deplete }}\r\n\r\n" "backupTime" = "🗄 وقت النسخة الاحتياطية: {{ .Time }}\r\n" "refreshedOn" = "\r\n📋🔄 اتحدّث في: {{ .Time }}\r\n\r\n" "yes" = "✅ أيوه" "no" = "❌ لأ" "received_id" = "🔑📥 الـ ID اتحدث." "received_password" = "🔑📥 الباسورد اتحدث." "received_email" = "📧📥 الإيميل اتحدث." "received_comment" = "💬📥 التعليق اتحدث." "id_prompt" = "🔑 الـ ID الافتراضي: {{ .ClientId }}\n\nادخل الـ ID بتاعك." "pass_prompt" = "🔑 الباسورد الافتراضي: {{ .ClientPassword }}\n\nادخل الباسورد بتاعك." "email_prompt" = "📧 الإيميل الافتراضي: {{ .ClientEmail }}\n\nادخل الإيميل بتاعك." "comment_prompt" = "💬 التعليق الافتراضي: {{ .ClientComment }}\n\nادخل تعليقك." "inbound_client_data_id" = "🔄 الدخول: {{ .InboundRemark }}\n\n🔑 المعرف: {{ .ClientId }}\n📧 البريد الإلكتروني: {{ .ClientEmail }}\n📊 الترافيك: {{ .ClientTraffic }}\n📅 تاريخ الانتهاء: {{ .ClientExp }}\n🌐 حدّ IP: {{ .IpLimit }}\n💬 تعليق: {{ .ClientComment }}\n\nدلوقتي تقدر تضيف العميل على الدخول!" "inbound_client_data_pass" = "🔄 الدخول: {{ .InboundRemark }}\n\n🔑 كلمة المرور: {{ .ClientPass }}\n📧 البريد الإلكتروني: {{ .ClientEmail }}\n📊 الترافيك: {{ .ClientTraffic }}\n📅 تاريخ الانتهاء: {{ .ClientExp }}\n🌐 حدّ IP: {{ .IpLimit }}\n💬 تعليق: {{ .ClientComment }}\n\nدلوقتي تقدر تضيف العميل على الدخول!" "cancel" = "❌ العملية اتلغت! \n\nممكن تبدأ من /start في أي وقت. 🔄" "error_add_client" = "⚠️ حصل خطأ:\n\n {{ .error }}" "using_default_value" = "تمام، هشيل على القيمة الافتراضية. 😊" "incorrect_input" = "المدخلات مش صحيحة.\nالكلمات لازم تكون متصلة من غير فراغات.\nمثال صحيح: aaaaaa\nمثال غلط: aaa aaa 🚫" "AreYouSure" = "إنت متأكد؟ 🤔" "SuccessResetTraffic" = "📧 البريد الإلكتروني: {{ .ClientEmail }}\n🏁 النتيجة: ✅ تم بنجاح" "FailedResetTraffic" = "📧 البريد الإلكتروني: {{ .ClientEmail }}\n🏁 النتيجة: ❌ فشل \n\n🛠️ الخطأ: [ {{ .ErrorMessage }} ]" "FinishProcess" = "🔚 عملية إعادة ضبط الترافيك خلصت لكل العملاء." [tgbot.buttons] "closeKeyboard" = "❌ اقفل الكيبورد" "cancel" = "❌ إلغاء" "cancelReset" = "❌ إلغاء إعادة الضبط" "cancelIpLimit" = "❌ إلغاء حد الـ IP" "confirmResetTraffic" = "✅ تأكيد إعادة ضبط الترافيك؟" "confirmClearIps" = "✅ تأكيد مسح الـ IPs؟" "confirmRemoveTGUser" = "✅ تأكيد حذف مستخدم Telegram؟" "confirmToggle" = "✅ تأكيد تفعيل/تعطيل المستخدم؟" "dbBackup" = "احصل على نسخة DB" "serverUsage" = "استخدام السيرفر" "getInbounds" = "احصل على الإدخالات" "depleteSoon" = "هينتهي قريب" "clientUsage" = "استخدام العميل" "onlines" = "العملاء الأونلاين" "commands" = "الأوامر" "refresh" = "🔄 تجديد" "clearIPs" = "❌ مسح الـ IPs" "removeTGUser" = "❌ حذف مستخدم Telegram" "selectTGUser" = "👤 اختار مستخدم Telegram" "selectOneTGUser" = "👤 اختار مستخدم Telegram:" "resetTraffic" = "📈 إعادة ضبط الترافيك" "resetExpire" = "📅 تغيير تاريخ الانتهاء" "ipLog" = "🔢 سجل الـ IP" "ipLimit" = "🔢 حد الـ IP" "setTGUser" = "👤 ضبط مستخدم Telegram" "toggle" = "🔘 تفعيل / تعطيل" "custom" = "🔢 مخصص" "confirmNumber" = "✅ تأكيد: {{ .Num }}" "confirmNumberAdd" = "✅ تأكيد إضافة: {{ .Num }}" "limitTraffic" = "🚧 حد الترافيك" "getBanLogs" = "احصل على سجلات الحظر" "allClients" = "كل العملاء" "addClient" = "إضافة عميل" "submitDisable" = "إرسال كمعطّل ☑️" "submitEnable" = "إرسال كمفعّل ✅" "use_default" = "🏷️ استخدام الإعدادات الافتراضية" "change_id" = "⚙️🔑 المعرّف" "change_password" = "⚙️🔑 كلمة السر" "change_email" = "⚙️📧 البريد الإلكتروني" "change_comment" = "⚙️💬 تعليق" "ResetAllTraffics" = "إعادة ضبط جميع الترافيك" "SortedTrafficUsageReport" = "تقرير استخدام الترافيك المرتب" [tgbot.answers] "successfulOperation" = "✅ العملية نجحت!" "errorOperation" = "❗ حصل خطأ في العملية." "getInboundsFailed" = "❌ فشل الحصول على الإدخالات." "getClientsFailed" = "❌ فشل الحصول على العملاء." "canceled" = "❌ {{ .Email }}: العملية اتلغت." "clientRefreshSuccess" = "✅ {{ .Email }}: العميل اتحدث بنجاح." "IpRefreshSuccess" = "✅ {{ .Email }}: الـ IPs اتحدثت بنجاح." "TGIdRefreshSuccess" = "✅ {{ .Email }}: مستخدم Telegram اتحدث بنجاح." "resetTrafficSuccess" = "✅ {{ .Email }}: الترافيك اتظبط بنجاح." "setTrafficLimitSuccess" = "✅ {{ .Email }}: حد الترافيك اتسجل بنجاح." "expireResetSuccess" = "✅ {{ .Email }}: أيام الانتهاء اتظبطت بنجاح." "resetIpSuccess" = "✅ {{ .Email }}: حد الـ IP ({{ .Count }}) اتسجل بنجاح." "clearIpSuccess" = "✅ {{ .Email }}: الـ IPs اتمسحت بنجاح." "getIpLog" = "✅ {{ .Email }}: سجل الـ IP اتجاب." "getUserInfo" = "✅ {{ .Email }}: بيانات مستخدم Telegram اتجاب." "removedTGUserSuccess" = "✅ {{ .Email }}: مستخدم Telegram اتحذف بنجاح." "enableSuccess" = "✅ {{ .Email }}: اتفعل بنجاح." "disableSuccess" = "✅ {{ .Email }}: اتعطل بنجاح." "askToAddUserId" = "مافيش إعدادات ليك!\r\nاطلب من الأدمن يضيف الـ Telegram ChatID الخاص بيك في إعداداتك.\r\n\r\nالـ ChatID بتاعك: {{ .TgUserID }}" "chooseClient" = "اختار عميل للإدخال {{ .Inbound }}" "chooseInbound" = "اختار الإدخال" ================================================ FILE: web/translation/translate.en_US.toml ================================================ "username" = "Username" "password" = "Password" "login" = "Log In" "confirm" = "Confirm" "cancel" = "Cancel" "close" = "Close" "create" = "Create" "update" = "Update" "copy" = "Copy" "copied" = "Copied" "download" = "Download" "remark" = "Remark" "enable" = "Enabled" "protocol" = "Protocol" "search" = "Search" "filter" = "Filter" "loading" = "Loading..." "second" = "Second" "minute" = "Minute" "hour" = "Hour" "day" = "Day" "check" = "Check" "indefinite" = "Indefinite" "unlimited" = "Unlimited" "none" = "None" "qrCode" = "QR Code" "info" = "More Information" "edit" = "Edit" "delete" = "Delete" "reset" = "Reset" "noData" = "No data." "copySuccess" = "Copied Successful" "sure" = "Sure" "encryption" = "Encryption" "useIPv4ForHost" = "Use IPv4 for host" "transmission" = "Transmission" "host" = "Host" "path" = "Path" "camouflage" = "Obfuscation" "status" = "Status" "enabled" = "Enabled" "disabled" = "Disabled" "depleted" = "Ended" "depletingSoon" = "Depleting" "offline" = "Offline" "online" = "Online" "domainName" = "Domain Name" "monitor" = "Listen IP" "certificate" = "Digital Certificate" "fail" = "Failed" "comment" = "Comment" "success" = "Successfully" "lastOnline" = "Last Online" "getVersion" = "Get Version" "install" = "Install" "clients" = "Clients" "usage" = "Usage" "twoFactorCode" = "Code" "remained" = "Remained" "security" = "Security" "secAlertTitle" = "Security Alert" "secAlertSsl" = "This connection is not secure. Please avoid entering sensitive information until TLS is activated for data protection." "secAlertConf" = "Certain settings are vulnerable to attacks. It is recommended to reinforce security protocols to prevent potential breaches." "secAlertSSL" = "Panel lacks secure connection. Please install TLS certificate for data protection." "secAlertPanelPort" = "Panel default port is vulnerable. Please configure a random or specific port." "secAlertPanelURI" = "Panel default URI path is insecure. Please configure a complex URI path." "secAlertSubURI" = "Subscription default URI path is insecure. Please configure a complex URI path." "secAlertSubJsonURI" = "Subscription JSON default URI path is insecure. Please configure a complex URI path." "emptyDnsDesc" = "No added DNS servers." "emptyFakeDnsDesc" = "No added Fake DNS servers." "emptyBalancersDesc" = "No added balancers." "emptyReverseDesc" = "No added reverse proxies." "somethingWentWrong" = "Something went wrong" [subscription] "title" = "Subscription info" "subId" = "Subscription ID" "status" = "Status" "downloaded" = "Downloaded" "uploaded" = "Uploaded" "expiry" = "Expiry" "totalQuota" = "Total quota" "individualLinks" = "Individual links" "active" = "Active" "inactive" = "Inactive" "unlimited" = "Unlimited" "noExpiry" = "No expiry" [menu] "theme" = "Theme" "dark" = "Dark" "ultraDark" = "Ultra Dark" "dashboard" = "Overview" "inbounds" = "Inbounds" "settings" = "Panel Settings" "xray" = "Xray Configs" "logout" = "Log Out" "link" = "Manage" [pages.login] "hello" = "Hello" "title" = "Welcome" "loginAgain" = "Your session has expired, please log in again" [pages.login.toasts] "invalidFormData" = "The Input data format is invalid." "emptyUsername" = "Username is required" "emptyPassword" = "Password is required" "wrongUsernameOrPassword" = "Invalid username or password or two-factor code." "successLogin" = " You have successfully logged into your account." [pages.index] "title" = "Overview" "cpu" = "CPU" "logicalProcessors" = "Logical Processors" "frequency" = "Frequency" "swap" = "Swap" "storage" = "Storage" "memory" = "RAM" "threads" = "Threads" "xrayStatus" = "Xray" "stopXray" = "Stop" "restartXray" = "Restart" "xraySwitch" = "Version" "xraySwitchClick" = "Choose the version you want to switch to." "xraySwitchClickDesk" = "Choose carefully, as older versions may not be compatible with current configurations." "xrayStatusUnknown" = "Unknown" "xrayStatusRunning" = "Running" "xrayStatusStop" = "Stop" "xrayStatusError" = "Error" "xrayErrorPopoverTitle" = "An error occurred while running Xray" "operationHours" = "Uptime" "systemLoad" = "System Load" "systemLoadDesc" = "System load average for the past 1, 5, and 15 minutes" "connectionCount" = "Connection Stats" "ipAddresses" = "IP Addresses" "toggleIpVisibility" = "Toggle visibility of the IP" "overallSpeed" = "Overall Speed" "upload" = "Upload" "download" = "Download" "totalData" = "Total Data" "sent" = "Sent" "received" = "Received" "documentation" = "Documentation" "xraySwitchVersionDialog" = "Do you really want to change the Xray version?" "xraySwitchVersionDialogDesc" = "This will change the Xray version to #version#." "xraySwitchVersionPopover" = "Xray updated successfully" "geofileUpdateDialog" = "Do you really want to update the geofile?" "geofileUpdateDialogDesc" = "This will update the #filename# file." "geofilesUpdateDialogDesc" = "This will update all geofiles." "geofilesUpdateAll" = "Update all" "geofileUpdatePopover" = "Geofile updated successfully" "dontRefresh" = "Installation is in progress, please do not refresh this page" "logs" = "Logs" "config" = "Config" "backup" = "Backup" "backupTitle" = "Database Backup & Restore" "exportDatabase" = "Back Up" "exportDatabaseDesc" = "Click to download a .db file containing a backup of your current database to your device." "importDatabase" = "Restore" "importDatabaseDesc" = "Click to select and upload a .db file from your device to restore your database from a backup." "importDatabaseSuccess" = "The database has been successfully imported." "importDatabaseError" = "An error occurred while importing the database." "readDatabaseError" = "An error occurred while reading the database." "getDatabaseError" = "An error occurred while retrieving the database." "getConfigError" = "An error occurred while retrieving the config file." [pages.inbounds] "allTimeTraffic" = "All-time Traffic" "allTimeTrafficUsage" = "All Time Total Usage" "title" = "Inbounds" "totalDownUp" = "Total Sent/Received" "totalUsage" = "Total Usage" "inboundCount" = "Total Inbounds" "operate" = "Menu" "enable" = "Enabled" "remark" = "Remark" "protocol" = "Protocol" "port" = "Port" "portMap" = "Port Mapping" "traffic" = "Traffic" "details" = "Details" "transportConfig" = "Transport" "expireDate" = "Duration" "createdAt" = "Created" "updatedAt" = "Updated" "resetTraffic" = "Reset Traffic" "addInbound" = "Add Inbound" "generalActions" = "General Actions" "autoRefresh" = "Auto-refresh" "autoRefreshInterval" = "Interval" "modifyInbound" = "Modify Inbound" "deleteInbound" = "Delete Inbound" "deleteInboundContent" = "Are you sure you want to delete inbound?" "deleteClient" = "Delete Client" "deleteClientContent" = "Are you sure you want to delete client?" "resetTrafficContent" = "Are you sure you want to reset traffic?" "copyLink" = "Copy URL" "address" = "Address" "network" = "Network" "destinationPort" = "Destination Port" "targetAddress" = "Target Address" "monitorDesc" = "Leave blank to listen on all IPs" "meansNoLimit" = "= Unlimited. (unit: GB)" "totalFlow" = "Total Flow" "leaveBlankToNeverExpire" = "Leave blank to never expire" "noRecommendKeepDefault" = "It is recommended to keep the default" "certificatePath" = "File Path" "certificateContent" = "File Content" "publicKey" = "Public Key" "privatekey" = "Private Key" "clickOnQRcode" = "Click on QR Code to Copy" "client" = "Client" "export" = "Export All URLs" "clone" = "Clone" "cloneInbound" = "Clone" "cloneInboundContent" = "All settings of this inbound, except Port, Listening IP, and Clients, will be applied to the clone." "cloneInboundOk" = "Clone" "resetAllTraffic" = "Reset All Inbounds Traffic" "resetAllTrafficTitle" = "Reset All Inbounds Traffic" "resetAllTrafficContent" = "Are you sure you want to reset the traffic of all inbounds?" "resetInboundClientTraffics" = "Reset Clients Traffic" "resetInboundClientTrafficTitle" = "Reset Clients Traffic" "resetInboundClientTrafficContent" = "Are you sure you want to reset the traffic of this inbound's clients?" "resetAllClientTraffics" = "Reset All Clients Traffic" "resetAllClientTrafficTitle" = "Reset All Clients Traffic" "resetAllClientTrafficContent" = "Are you sure you want to reset the traffic of all clients?" "delDepletedClients" = "Delete Depleted Clients" "delDepletedClientsTitle" = "Delete Depleted Clients" "delDepletedClientsContent" = "Are you sure you want to delete all the depleted clients?" "email" = "Email" "emailDesc" = "Please provide a unique email address." "IPLimit" = "IP Limit" "IPLimitDesc" = "Disables inbound if the count exceeds the set value. (0 = disable)" "IPLimitlog" = "IP Log" "IPLimitlogDesc" = "The IPs history log. (to enable inbound after disabling, clear the log)" "IPLimitlogclear" = "Clear The Log" "setDefaultCert" = "Set Cert from Panel" "telegramDesc" = "Please provide Telegram Chat ID. (use '/id' command in the bot) or (@userinfobot)" "subscriptionDesc" = "To find your subscription URL, navigate to the 'Details'. Additionally, you can use the same name for several clients." "info" = "Info" "same" = "Same" "inboundData" = "Inbound's Data" "exportInbound" = "Export Inbound" "import" = "Import" "importInbound" = "Import an Inbound" "periodicTrafficResetTitle" = "Traffic Reset" "periodicTrafficResetDesc" = "Automatically reset traffic counter at specified intervals" "lastReset" = "Last Reset" [pages.client] "add" = "Add Client" "edit" = "Edit Client" "submitAdd" = "Add Client" "submitEdit" = "Save Changes" "clientCount" = "Number of Clients" "bulk" = "Add Bulk" "method" = "Method" "first" = "First" "last" = "Last" "prefix" = "Prefix" "postfix" = "Postfix" "delayedStart" = "Start After First Use" "expireDays" = "Duration" "days" = "Day(s)" "renew" = "Auto Renew" "renewDesc" = "Auto-renewal after expiration. (0 = disable)(unit: day)" [pages.inbounds.periodicTrafficReset] "never" = "Never" "daily" = "Daily" "weekly" = "Weekly" "monthly" = "Monthly" [pages.inbounds.toasts] "obtain" = "Obtain" "updateSuccess" = "The update was successful." "logCleanSuccess" = "The log has been cleared." "inboundsUpdateSuccess" = "Inbounds have been successfully updated." "inboundUpdateSuccess" = "Inbound has been successfully updated." "inboundCreateSuccess" = "Inbound has been successfully created." "inboundDeleteSuccess" = "Inbound has been successfully deleted." "inboundClientAddSuccess" = "Inbound client(s) have been added." "inboundClientDeleteSuccess" = "Inbound client has been deleted." "inboundClientUpdateSuccess" = "Inbound client has been updated." "delDepletedClientsSuccess" = "All depleted clients are deleted." "resetAllClientTrafficSuccess" = "All traffic from the client has been reset." "resetAllTrafficSuccess" = "All traffic has been reset." "resetInboundClientTrafficSuccess" = "Traffic has been reset." "trafficGetError" = "Error getting traffics." "getNewX25519CertError" = "Error while obtaining the X25519 certificate." "getNewmldsa65Error" = "Error while obtaining mldsa65." "getNewVlessEncError" = "Error while obtaining VlessEnc." [pages.inbounds.stream.general] "request" = "Request" "response" = "Response" "name" = "Name" "value" = "Value" [pages.inbounds.stream.tcp] "version" = "Version" "method" = "Method" "path" = "Path" "status" = "Status" "statusDescription" = "Status Desc" "requestHeader" = "Request Header" "responseHeader" = "Response Header" [pages.settings] "title" = "Panel Settings" "save" = "Save" "infoDesc" = "Every change made here needs to be saved. Please restart the panel to apply changes." "restartPanel" = "Restart Panel" "restartPanelDesc" = "Are you sure you want to restart the panel? If you cannot access the panel after restarting, please view the panel log info on the server." "restartPanelSuccess" = "The panel was successfully restarted." "actions" = "Actions" "resetDefaultConfig" = "Reset to Default" "panelSettings" = "General" "securitySettings" = "Authentication" "TGBotSettings" = "Telegram Bot" "panelListeningIP" = "Listen IP" "panelListeningIPDesc" = "The IP address for the web panel. (leave blank to listen on all IPs)" "panelListeningDomain" = "Listen Domain" "panelListeningDomainDesc" = "The domain name for the web panel. (leave blank to listen on all domains and IPs)" "panelPort" = "Listen Port" "panelPortDesc" = "The port number for the web panel. (must be an unused port)" "publicKeyPath" = "Public Key Path" "publicKeyPathDesc" = "The public key file path for the web panel. (begins with ‘/‘)" "privateKeyPath" = "Private Key Path" "privateKeyPathDesc" = "The private key file path for the web panel. (begins with ‘/‘)" "panelUrlPath" = "URI Path" "panelUrlPathDesc" = "The URI path for the web panel. (begins with ‘/‘ and concludes with ‘/‘)" "pageSize" = "Pagination Size" "pageSizeDesc" = "Define page size for inbounds table. (0 = disable)" "remarkModel" = "Remark Model & Separation Character" "datepicker" = "Calendar Type" "datepickerPlaceholder" = "Select date" "datepickerDescription" = "Scheduled tasks will run based on this calendar." "sampleRemark" = "Sample Remark" "oldUsername" = "Current Username" "currentPassword" = "Current Password" "newUsername" = "New Username" "newPassword" = "New Password" "telegramBotEnable" = "Enable Telegram Bot" "telegramBotEnableDesc" = "Enables the Telegram bot." "telegramToken" = "Telegram Token" "telegramTokenDesc" = "The Telegram bot token obtained from '@BotFather'." "telegramProxy" = "SOCKS Proxy" "telegramProxyDesc" = "Enables SOCKS5 proxy for connecting to Telegram. (adjust settings as per guide)" "telegramAPIServer" = "Telegram API Server" "telegramAPIServerDesc" = "The Telegram API server to use. Leave blank to use the default server." "telegramChatId" = "Admin Chat ID" "telegramChatIdDesc" = "The Telegram Admin Chat ID(s). (comma-separated)(get it here @userinfobot) or (use '/id' command in the bot)" "telegramNotifyTime" = "Notification Time" "telegramNotifyTimeDesc" = "The Telegram bot notification time set for periodic reports. (use the crontab time format)" "tgNotifyBackup" = "Database Backup" "tgNotifyBackupDesc" = "Send a database backup file with a report." "tgNotifyLogin" = "Login Notification" "tgNotifyLoginDesc" = "Get notified about the username, IP address, and time whenever someone attempts to log into your web panel." "sessionMaxAge" = "Session Duration" "sessionMaxAgeDesc" = "The duration for which you can stay logged in. (unit: minute)" "expireTimeDiff" = "Expiration Date Notification" "expireTimeDiffDesc" = "Get notified about expiration date when reaching this threshold. (unit: day)" "trafficDiff" = "Traffic Cap Notification" "trafficDiffDesc" = "Get notified about traffic cap when reaching this threshold. (unit: GB)" "tgNotifyCpu" = "CPU Load Notification" "tgNotifyCpuDesc" = "Get notified if CPU load exceeds this threshold. (unit: %)" "timeZone" = "Time Zone" "timeZoneDesc" = "Scheduled tasks will run based on this time zone." "subSettings" = "Subscription" "subEnable" = "Subscription Service" "subEnableDesc" = "Enable/Disable the subscription service." "subJsonEnable" = "Enable/Disable the JSON subscription endpoint independently." "subTitle" = "Subscription Title" "subTitleDesc" = "Title shown in VPN client" "subSupportUrl" = "Support URL" "subSupportUrlDesc" = "Technical support link shown in the VPN client" "subProfileUrl" = "Profile URL" "subProfileUrlDesc" = "A link to your website displayed in the VPN client" "subAnnounce" = "Announce" "subAnnounceDesc" = "The text of the announce displayed in the VPN client" "subEnableRouting" = "Enable routing" "subEnableRoutingDesc" = "Global setting to enable routing in the VPN client. (Only for Happ)" "subRoutingRules" = "Routing rules" "subRoutingRulesDesc" = "Global routing rules for the VPN client. (Only for Happ)" "subListen" = "Listen IP" "subListenDesc" = "The IP address for the subscription service. (leave blank to listen on all IPs)" "subPort" = "Listen Port" "subPortDesc" = "The port number for the subscription service. (must be an unused port)" "subCertPath" = "Public Key Path" "subCertPathDesc" = "The public key file path for the subscription service. (begins with ‘/‘)" "subKeyPath" = "Private Key Path" "subKeyPathDesc" = "The private key file path for the subscription service. (begins with ‘/‘)" "subPath" = "URI Path" "subPathDesc" = "The URI path for the subscription service. (begins with ‘/‘ and concludes with ‘/‘)" "subDomain" = "Listen Domain" "subDomainDesc" = "The domain name for the subscription service. (leave blank to listen on all domains and IPs)" "subUpdates" = "Update Intervals" "subUpdatesDesc" = "The update intervals of the subscription URL in the client apps. (unit: hour)" "subEncrypt" = "Encode" "subEncryptDesc" = "The returned content of subscription service will be Base64 encoded." "subShowInfo" = "Show Usage Info" "subShowInfoDesc" = "The remaining traffic and date will be displayed in the client apps." "subURI" = "Reverse Proxy URI" "subURIDesc" = "The URI path of the subscription URL for use behind proxies." "externalTrafficInformEnable" = "External Traffic Inform" "externalTrafficInformEnableDesc" = "Inform external API on every traffic update." "externalTrafficInformURI" = "External Traffic Inform URI" "externalTrafficInformURIDesc" = "Traffic updates are sent to this URI." "fragment" = "Fragmentation" "fragmentDesc" = "Enable fragmentation for TLS hello packet." "fragmentSett" = "Fragmentation Settings" "noisesDesc" = "Enable Noises." "noisesSett" = "Noises Settings" "mux" = "Mux" "muxDesc" = "Transmit multiple independent data streams within an established data stream." "muxSett" = "Mux Settings" "direct" = "Direct Connection" "directDesc" = "Directly establishes connections with domains or IP ranges of a specific country." "notifications" = "Notifications" "certs" = "Certificaties" "externalTraffic" = "External Traffic" "dateAndTime" = "Date and Time" "proxyAndServer" = "Proxy and Server" "intervals" = "Intervals" "information" = "Information" "language" = "Language" "telegramBotLanguage" = "Telegram Bot Language" [pages.xray] "title" = "Xray Configs" "save" = "Save" "restart" = "Restart Xray" "restartSuccess" = "Xray has been successfully relaunched." "stopSuccess" = "Xray has been successfully stopped." "restartError" = "There was an error when rebooting the Xray." "stopError" = "There was an error when stopping the Xray." "basicTemplate" = "Basics" "advancedTemplate" = "Advanced" "generalConfigs" = "General" "generalConfigsDesc" = "These options will determine general adjustments." "logConfigs" = "Log" "logConfigsDesc" = "Logs may affect your server's efficiency. It is recommended to enable it wisely only in case of your needs" "blockConfigsDesc" = "These options will block traffic based on specific requested protocols and websites." "basicRouting" = "Basic Routing" "blockConnectionsConfigsDesc" = "These options will block traffic based on the specific requested country." "directConnectionsConfigsDesc" = "A direct connection ensures that specific traffic is not routed through another server." "blockips" = "Block IPs" "blockdomains" = "Block Domains" "directips" = "Direct IPs" "directdomains" = "Direct Domains" "ipv4Routing" = "IPv4 Routing" "ipv4RoutingDesc" = "These options will route traffic based on a specific destination via IPv4." "warpRouting" = "WARP Routing" "warpRoutingDesc" = "These options will route traffic based on a specific destination via WARP." "Template" = "Advanced Xray Configuration Template" "TemplateDesc" = "The final Xray config file will be generated based on this template." "FreedomStrategy" = "Freedom Protocol Strategy" "FreedomStrategyDesc" = "Set the output strategy for the network in the Freedom Protocol." "RoutingStrategy" = "Overall Routing Strategy" "RoutingStrategyDesc" = "Set the overall traffic routing strategy for resolving all requests." "outboundTestUrl" = "Outbound Test URL" "outboundTestUrlDesc" = "URL used when testing outbound connectivity." "Torrent" = "Block BitTorrent Protocol" "Inbounds" = "Inbounds" "InboundsDesc" = "Accepting the specific clients." "Outbounds" = "Outbounds" "Balancers" = "Balancers" "OutboundsDesc" = "Set the outgoing traffic pathway." "Routings" = "Routing Rules" "RoutingsDesc" = "The priority of each rule is important!" "completeTemplate" = "All" "logLevel" = "Log Level" "logLevelDesc" = "The log level for error logs, indicating the information that needs to be recorded." "accessLog" = "Access Log" "accessLogDesc" = "The file path for the access log. The special value 'none' disabled access logs" "errorLog" = "Error Log" "errorLogDesc" = "The file path for the error log. The special value 'none' disabled error logs" "dnsLog" = "DNS Log" "dnsLogDesc" = "Whether to enable DNS query logs" "maskAddress" = "Mask Address" "maskAddressDesc" = "IP address mask, when enabled, will automatically replace the IP address that appears in the log." "statistics" = "Statistics" "statsInboundUplink" = "Inbound Upload Statistics" "statsInboundUplinkDesc" = "Enables the statistics collection for upstream traffic of all inbound proxies." "statsInboundDownlink" = "Inbound Download Statistics" "statsInboundDownlinkDesc" = "Enables the statistics collection for downstream traffic of all inbound proxies." "statsOutboundUplink" = "Outbound Upload Statistics" "statsOutboundUplinkDesc" = "Enables the statistics collection for upstream traffic of all outbound proxies." "statsOutboundDownlink" = "Outbound Download Statistics" "statsOutboundDownlinkDesc" = "Enables the statistics collection for downstream traffic of all outbound proxies." [pages.xray.rules] "first" = "First" "last" = "Last" "up" = "Up" "down" = "Down" "source" = "Source" "dest" = "Destination" "inbound" = "Inbound" "outbound" = "Outbound" "balancer" = "Balancer" "info" = "Info" "add" = "Add Rule" "edit" = "Edit Rule" "useComma" = "Comma-separated items" [pages.xray.outbound] "addOutbound" = "Add Outbound" "addReverse" = "Add Reverse" "editOutbound" = "Edit Outbound" "editReverse" = "Edit Reverse" "tag" = "Tag" "tagDesc" = "Unique Tag" "address" = "Address" "reverse" = "Reverse" "domain" = "Domain" "type" = "Type" "bridge" = "Bridge" "portal" = "Portal" "link" = "Link" "intercon" = "Interconnection" "settings" = "Settings" "accountInfo" = "Account Information" "outboundStatus" = "Outbound Status" "sendThrough" = "Send Through" "test" = "Test" "testResult" = "Test Result" "testing" = "Testing connection..." "testSuccess" = "Test successful" "testFailed" = "Test failed" "testError" = "Failed to test outbound" [pages.xray.balancer] "addBalancer" = "Add Balancer" "editBalancer" = "Edit Balancer" "balancerStrategy" = "Strategy" "balancerSelectors" = "Selectors" "tag" = "Tag" "tagDesc" = "Unique Tag" "balancerDesc" = "It is not possible to use balancerTag and outboundTag at the same time. If used at the same time, only outboundTag will work." [pages.xray.wireguard] "secretKey" = "Secret Key" "publicKey" = "Public Key" "allowedIPs" = "Allowed IPs" "endpoint" = "Endpoint" "psk" = "PreShared Key" "domainStrategy" = "Domain Strategy" [pages.xray.tun] "nameDesc" = "The name of the TUN interface. Default is 'xray0'" "mtuDesc" = "Maximum Transmission Unit. The maximum size of data packets. Default is 1500" "userLevel" = "User Level" "userLevelDesc" = "All connections made through this inbound will use this user level. Default is 0" [pages.xray.dns] "enable" = "Enable DNS" "enableDesc" = "Enable built-in DNS server" "tag" = "DNS Inbound Tag" "tagDesc" = "This tag will be available as an Inbound tag in routing rules." "clientIp" = "Client IP" "clientIpDesc" = "Used to notify the server of the specified IP location during DNS queries" "disableCache" = "Disable cache" "disableCacheDesc" = "Disables DNS caching" "disableFallback" = "Disable Fallback" "disableFallbackDesc" = "Disables fallback DNS queries" "disableFallbackIfMatch" = "Disable Fallback If Match" "disableFallbackIfMatchDesc" = "Disables fallback DNS queries when the matching domain list of the DNS server is hit" "enableParallelQuery" = "Enable Parallel Query" "enableParallelQueryDesc" = "Enable parallel DNS queries to multiple servers for faster resolution" "strategy" = "Query Strategy" "strategyDesc" = "Overall strategy to resolve domain names" "add" = "Add Server" "edit" = "Edit Server" "domains" = "Domains" "expectIPs" = "Expect IPs" "unexpectIPs" = "Unexpect IPs" "useSystemHosts" = "Use System Hosts" "useSystemHostsDesc" = "Use the hosts file from an installed system" "usePreset" = "Use Preset" "dnsPresetTitle" = "DNS Presets" "dnsPresetFamily" = "Family" [pages.xray.fakedns] "add" = "Add Fake DNS" "edit" = "Edit Fake DNS" "ipPool" = "IP Pool Subnet" "poolSize" = "Pool Size" [pages.settings.security] "admin" = "Admin credentials" "twoFactor" = "Two-factor authentication" "twoFactorEnable" = "Enable 2FA" "twoFactorEnableDesc" = "Adds an additional layer of authentication to provide more security." "twoFactorModalSetTitle" = "Enable two-factor authentication" "twoFactorModalDeleteTitle" = "Disable two-factor authentication" "twoFactorModalSteps" = "To set up two-factor authentication, perform a few steps:" "twoFactorModalFirstStep" = "1. Scan this QR code in the app for authentication or copy the token near the QR code and paste it into the app" "twoFactorModalSecondStep" = "2. Enter the code from the app" "twoFactorModalRemoveStep" = "Enter the code from the application to remove two-factor authentication." "twoFactorModalChangeCredentialsTitle" = "Change credentials" "twoFactorModalChangeCredentialsStep" = "Enter the code from the application to change administrator credentials." "twoFactorModalSetSuccess" = "Two-factor authentication has been successfully established" "twoFactorModalDeleteSuccess" = "Two-factor authentication has been successfully deleted" "twoFactorModalError" = "Wrong code" [pages.settings.toasts] "modifySettings" = "The parameters have been changed." "getSettings" = "An error occurred while retrieving parameters." "modifyUserError" = "An error occurred while changing administrator credentials." "modifyUser" = "You have successfully changed the credentials of the administrator." "originalUserPassIncorrect" = "The сurrent username or password is invalid" "userPassMustBeNotEmpty" = "The new username and password is empty" "getOutboundTrafficError" = "Error getting traffics" "resetOutboundTrafficError" = "Error in reset outbound traffics" [tgbot] "keyboardClosed" = "❌ Custom keyboard closed!" "noResult" = "❗ No result!" "noQuery" = "❌ Query not found! Please use the command again!" "wentWrong" = "❌ Something went wrong!" "noIpRecord" = "❗ No IP Record!" "noInbounds" = "❗ No inbound found!" "unlimited" = "♾ Unlimited(Reset)" "add" = "Add" "month" = "Month" "months" = "Months" "day" = "Day" "days" = "Days" "hours" = "Hours" "minutes" = "Minutes" "unknown" = "Unknown" "inbounds" = "Inbounds" "clients" = "Clients" "offline" = "🔴 Offline" "online" = "🟢 Online" [tgbot.commands] "unknown" = "❗ Unknown command." "pleaseChoose" = "👇 Please choose:\r\n" "help" = "🤖 Welcome to this bot! It's designed to offer specific data from the web panel and allows you to make modifications as needed.\r\n\r\n" "start" = "👋 Hello {{ .Firstname }}.\r\n" "welcome" = "🤖 Welcome to {{ .Hostname }} management bot.\r\n" "status" = "✅ Bot is OK!" "usage" = "❗ Please provide a text to search!" "getID" = "🆔 Your ID: {{ .ID }}" "helpAdminCommands" = "To restart Xray Core:\r\n/restart\r\n\r\nTo search for a client email:\r\n/usage [Email]\r\n\r\nTo search for inbounds (with client stats):\r\n/inbound [Remark]\r\n\r\nTelegram Chat ID:\r\n/id" "helpClientCommands" = "To search for statistics, use the following command:\r\n\r\n/usage [Email]\r\n\r\nTelegram Chat ID:\r\n/id" "restartUsage" = "\r\n\r\n/restart" "restartSuccess" = "✅ Operation successful!" "restartFailed" = "❗ Error in operation.\r\n\r\nError: {{ .Error }}." "xrayNotRunning" = "❗ Xray Core is not running." "startDesc" = "Show the main menu" "helpDesc" = "Bot help" "statusDesc" = "Check bot status" "idDesc" = "Show your Telegram ID" [tgbot.messages] "cpuThreshold" = "🔴 CPU Load {{ .Percent }}% exceeds the threshold of {{ .Threshold }}%" "selectUserFailed" = "❌ Error in user selection!" "userSaved" = "✅ Telegram User saved." "loginSuccess" = "✅ Logged in to the panel successfully.\r\n" "loginFailed" = "❗️Login attempt to the panel failed.\r\n" "2faFailed" = "2FA Failed" "report" = "🕰 Scheduled Reports: {{ .RunTime }}\r\n" "datetime" = "⏰ Date&Time: {{ .DateTime }}\r\n" "hostname" = "💻 Host: {{ .Hostname }}\r\n" "version" = "🚀 3X-UI Version: {{ .Version }}\r\n" "xrayVersion" = "📡 Xray Version: {{ .XrayVersion }}\r\n" "ipv6" = "🌐 IPv6: {{ .IPv6 }}\r\n" "ipv4" = "🌐 IPv4: {{ .IPv4 }}\r\n" "ip" = "🌐 IP: {{ .IP }}\r\n" "ips" = "🔢 IPs:\r\n{{ .IPs }}\r\n" "serverUpTime" = "⏳ Uptime: {{ .UpTime }} {{ .Unit }}\r\n" "serverLoad" = "📈 System Load: {{ .Load1 }}, {{ .Load2 }}, {{ .Load3 }}\r\n" "serverMemory" = "📋 RAM: {{ .Current }}/{{ .Total }}\r\n" "tcpCount" = "🔹 TCP: {{ .Count }}\r\n" "udpCount" = "🔸 UDP: {{ .Count }}\r\n" "traffic" = "🚦 Traffic: {{ .Total }} (↑{{ .Upload }},↓{{ .Download }})\r\n" "xrayStatus" = "ℹ️ Status: {{ .State }}\r\n" "username" = "👤 Username: {{ .Username }}\r\n" "password" = "👤 Password: {{ .Password }}\r\n" "time" = "⏰ Time: {{ .Time }}\r\n" "inbound" = "📍 Inbound: {{ .Remark }}\r\n" "port" = "🔌 Port: {{ .Port }}\r\n" "expire" = "📅 Expire Date: {{ .Time }}\r\n" "expireIn" = "📅 Expire In: {{ .Time }}\r\n" "active" = "💡 Active: {{ .Enable }}\r\n" "enabled" = "🚨 Enabled: {{ .Enable }}\r\n" "online" = "🌐 Connection status: {{ .Status }}\r\n" "lastOnline" = "🔙 Last online: {{ .Time }}\r\n" "email" = "📧 Email: {{ .Email }}\r\n" "upload" = "🔼 Upload: ↑{{ .Upload }}\r\n" "download" = "🔽 Download: ↓{{ .Download }}\r\n" "total" = "📊 Total: ↑↓{{ .UpDown }} / {{ .Total }}\r\n" "TGUser" = "👤 Telegram User: {{ .TelegramID }}\r\n" "exhaustedMsg" = "🚨 Exhausted {{ .Type }}:\r\n" "exhaustedCount" = "🚨 Exhausted {{ .Type }} count:\r\n" "onlinesCount" = "🌐 Online Clients: {{ .Count }}\r\n" "disabled" = "🛑 Disabled: {{ .Disabled }}\r\n" "depleteSoon" = "🔜 Deplete Soon: {{ .Deplete }}\r\n\r\n" "backupTime" = "🗄 Backup Time: {{ .Time }}\r\n" "refreshedOn" = "\r\n📋🔄 Refreshed On: {{ .Time }}\r\n\r\n" "yes" = "✅ Yes" "no" = "❌ No" "received_id" = "🔑📥 ID updated." "received_password" = "🔑📥 Password updated." "received_email" = "📧📥 Email updated." "received_comment" = "💬📥 Comment updated." "id_prompt" = "🔑 Default ID: {{ .ClientId }}\n\nEnter your id." "pass_prompt" = "🔑 Default Password: {{ .ClientPassword }}\n\nEnter your password." "email_prompt" = "📧 Default Email: {{ .ClientEmail }}\n\nEnter your email." "comment_prompt" = "💬 Default Comment: {{ .ClientComment }}\n\nEnter your Comment." "inbound_client_data_id" = "🔄 Inbound: {{ .InboundRemark }}\n\n🔑 ID: {{ .ClientId }}\n📧 Email: {{ .ClientEmail }}\n📊 Traffic: {{ .ClientTraffic }}\n📅 Expire Date: {{ .ClientExp }}\n🌐 IP Limit: {{ .IpLimit }}\n💬 Comment: {{ .ClientComment }}\n\nYou can add the client to inbound now!" "inbound_client_data_pass" = "🔄 Inbound: {{ .InboundRemark }}\n\n🔑 Password: {{ .ClientPass }}\n📧 Email: {{ .ClientEmail }}\n📊 Traffic: {{ .ClientTraffic }}\n📅 Expire Date: {{ .ClientExp }}\n🌐 IP Limit: {{ .IpLimit }}\n💬 Comment: {{ .ClientComment }}\n\nYou can add the client to inbound now!" "cancel" = "❌ Process Canceled! \n\nYou can /start again anytime. 🔄" "error_add_client" = "⚠️ Error:\n\n {{ .error }}" "using_default_value" = "Okay, I'll stick with the default value. 😊" "incorrect_input" = "Your input is not valid.\nThe phrases should be continuous without spaces.\nCorrect example: aaaaaa\nIncorrect example: aaa aaa 🚫" "AreYouSure" = "Are you sure? 🤔" "SuccessResetTraffic" = "📧 Email: {{ .ClientEmail }}\n🏁 Result: ✅ Success" "FailedResetTraffic" = "📧 Email: {{ .ClientEmail }}\n🏁 Result: ❌ Failed \n\n🛠️ Error: [ {{ .ErrorMessage }} ]" "FinishProcess" = "🔚 Traffic reset process finished for all clients." [tgbot.buttons] "closeKeyboard" = "❌ Close Keyboard" "cancel" = "❌ Cancel" "cancelReset" = "❌ Cancel Reset" "cancelIpLimit" = "❌ Cancel IP Limit" "confirmResetTraffic" = "✅ Confirm Reset Traffic?" "confirmClearIps" = "✅ Confirm Clear IPs?" "confirmRemoveTGUser" = "✅ Confirm Remove Telegram User?" "confirmToggle" = "✅ Confirm Enable/Disable User?" "dbBackup" = "Get DB Backup" "serverUsage" = "Server Usage" "getInbounds" = "Get Inbounds" "depleteSoon" = "Deplete Soon" "clientUsage" = "Get Usage" "onlines" = "Online Clients" "commands" = "Commands" "refresh" = "🔄 Refresh" "clearIPs" = "❌ Clear IPs" "removeTGUser" = "❌ Remove Telegram User" "selectTGUser" = "👤 Select Telegram User" "selectOneTGUser" = "👤 Select a Telegram User:" "resetTraffic" = "📈 Reset Traffic" "resetExpire" = "📅 Change Expiry Date" "ipLog" = "🔢 IP Log" "ipLimit" = "🔢 IP Limit" "setTGUser" = "👤 Set Telegram User" "toggle" = "🔘 Enable / Disable" "custom" = "🔢 Custom" "confirmNumber" = "✅ Confirm: {{ .Num }}" "confirmNumberAdd" = "✅ Confirm adding: {{ .Num }}" "limitTraffic" = "🚧 Traffic Limit" "getBanLogs" = "Get Ban Logs" "allClients" = "All Clients" "addClient" = "Add Client" "submitDisable" = "Submit As Disable ☑️" "submitEnable" = "Submit As Enable ✅" "use_default" = "🏷️ Use default" "change_id" = "⚙️🔑 ID" "change_password" = "⚙️🔑 Password" "change_email" = "⚙️📧 Email" "change_comment" = "⚙️💬 Comment" "ResetAllTraffics" = "Reset All Traffics" "SortedTrafficUsageReport" = "Sorted Traffic Usage Report" [tgbot.answers] "successfulOperation" = "✅ Operation successful!" "errorOperation" = "❗ Error in operation." "getInboundsFailed" = "❌ Failed to get inbounds." "getClientsFailed" = "❌ Failed to get clients." "canceled" = "❌ {{ .Email }}: Operation canceled." "clientRefreshSuccess" = "✅ {{ .Email }}: Client refreshed successfully." "IpRefreshSuccess" = "✅ {{ .Email }}: IPs refreshed successfully." "TGIdRefreshSuccess" = "✅ {{ .Email }}: Client's Telegram User refreshed successfully." "resetTrafficSuccess" = "✅ {{ .Email }}: Traffic reset successfully." "setTrafficLimitSuccess" = "✅ {{ .Email }}: Traffic limit saved successfully." "expireResetSuccess" = "✅ {{ .Email }}: Expire days reset successfully." "resetIpSuccess" = "✅ {{ .Email }}: IP limit {{ .Count }} saved successfully." "clearIpSuccess" = "✅ {{ .Email }}: IPs cleared successfully." "getIpLog" = "✅ {{ .Email }}: Get IP Log." "getUserInfo" = "✅ {{ .Email }}: Get Telegram User Info." "removedTGUserSuccess" = "✅ {{ .Email }}: Telegram User removed successfully." "enableSuccess" = "✅ {{ .Email }}: Enabled successfully." "disableSuccess" = "✅ {{ .Email }}: Disabled successfully." "askToAddUserId" = "Your configuration is not found!\r\nPlease ask your admin to use your Telegram ChatID in your configuration(s).\r\n\r\nYour ChatID: {{ .TgUserID }}" "chooseClient" = "Choose a Client for Inbound {{ .Inbound }}" "chooseInbound" = "Choose an Inbound" ================================================ FILE: web/translation/translate.es_ES.toml ================================================ "username" = "Nombre de Usuario" "password" = "Contraseña" "login" = "Acceder" "confirm" = "Confirmar" "cancel" = "Cancelar" "close" = "Cerrar" "create" = "Crear" "update" = "Actualizar" "copy" = "Copiar" "copied" = "Copiado" "download" = "Descargar" "remark" = "Notas" "enable" = "Habilitar" "protocol" = "Protocolo" "search" = "Buscar" "filter" = "Filtrar" "loading" = "Cargando..." "second" = "Segundo" "minute" = "Minuto" "hour" = "Hora" "day" = "Día" "check" = "Verificar" "indefinite" = "Indefinido" "unlimited" = "Ilimitado" "none" = "None" "qrCode" = "Código QR" "info" = "Más Información" "edit" = "Editar" "delete" = "Eliminar" "reset" = "Restablecer" "noData" = "Sin datos" "copySuccess" = "Copiado exitosamente" "sure" = "Seguro" "encryption" = "Encriptación" "useIPv4ForHost" = "Usar IPv4 para el host" "transmission" = "Transmisión" "host" = "Host" "path" = "Path" "camouflage" = "Camuflaje" "status" = "Estado" "enabled" = "Habilitado" "disabled" = "Deshabilitado" "depleted" = "Agotado" "depletingSoon" = "Agotándose" "offline" = "fuera de línea" "online" = "en línea" "domainName" = "Nombre de dominio" "monitor" = "Listening IP" "certificate" = "Certificado Digital" "fail" = "Falló" "comment" = "Comentario" "success" = "Éxito" "lastOnline" = "Última conexión" "getVersion" = "Obtener versión" "install" = "Instalar" "clients" = "Clientes" "usage" = "Uso" "twoFactorCode" = "Código" "remained" = "Restante" "security" = "Seguridad" "secAlertTitle" = "Alerta de Seguridad" "secAlertSsl" = "Esta conexión no es segura. Por favor, evite ingresar información sensible hasta que se active TLS para la protección de datos." "secAlertConf" = "Ciertas configuraciones son vulnerables a ataques. Se recomienda reforzar los protocolos de seguridad para prevenir posibles violaciones." "secAlertSSL" = "El panel carece de una conexión segura. Por favor, instale un certificado TLS para la protección de datos." "secAlertPanelPort" = "El puerto predeterminado del panel es vulnerable. Por favor, configure un puerto aleatorio o específico." "secAlertPanelURI" = "La ruta URI predeterminada del panel no es segura. Por favor, configure una ruta URI compleja." "secAlertSubURI" = "La ruta URI predeterminada de la suscripción no es segura. Por favor, configure una ruta URI compleja." "secAlertSubJsonURI" = "La ruta URI JSON predeterminada de la suscripción no es segura. Por favor, configure una ruta URI compleja." "emptyDnsDesc" = "No hay servidores DNS añadidos." "emptyFakeDnsDesc" = "No hay servidores Fake DNS añadidos." "emptyBalancersDesc" = "No hay balanceadores añadidos." "emptyReverseDesc" = "No hay proxies inversos añadidos." "somethingWentWrong" = "Algo salió mal" [subscription] "title" = "Información de suscripción" "subId" = "ID de suscripción" "status" = "Estado" "downloaded" = "Descargado" "uploaded" = "Subido" "expiry" = "Caducidad" "totalQuota" = "Cuota total" "individualLinks" = "Enlaces individuales" "active" = "Activo" "inactive" = "Inactivo" "unlimited" = "Ilimitado" "noExpiry" = "Sin caducidad" [menu] "theme" = "Tema" "dark" = "Oscuro" "ultraDark" = "Ultra Oscuro" "dashboard" = "Estado del Sistema" "inbounds" = "Entradas" "settings" = "Configuraciones" "xray" = "Ajustes Xray" "logout" = "Cerrar Sesión" "link" = "Gestionar" [pages.login] "hello" = "Hola" "title" = "Bienvenido" "loginAgain" = "El límite de tiempo de inicio de sesión ha expirado. Por favor, inicia sesión nuevamente." [pages.login.toasts] "invalidFormData" = "El formato de los datos de entrada es inválido." "emptyUsername" = "Por favor ingresa el nombre de usuario." "emptyPassword" = "Por favor ingresa la contraseña." "wrongUsernameOrPassword" = "Nombre de usuario, contraseña o código de dos factores incorrecto." "successLogin" = "Has iniciado sesión en tu cuenta correctamente." [pages.index] "title" = "Estado del Sistema" "cpu" = "CPU" "logicalProcessors" = "Procesadores lógicos" "frequency" = "Frecuencia" "swap" = "Memoria Virtual" "storage" = "Almacenamiento" "memory" = "RAM" "threads" = "Hilos" "xrayStatus" = "Xray" "stopXray" = "Detener" "restartXray" = "Reiniciar" "xraySwitch" = "Versión" "xraySwitchClick" = "Elige la versión a la que deseas cambiar." "xraySwitchClickDesk" = "Elige sabiamente, ya que las versiones anteriores pueden no ser compatibles con las configuraciones actuales." "xrayStatusUnknown" = "Desconocido" "xrayStatusRunning" = "En ejecución" "xrayStatusStop" = "Detenido" "xrayStatusError" = "Error" "xrayErrorPopoverTitle" = "Se produjo un error al ejecutar Xray" "operationHours" = "Tiempo de Funcionamiento" "systemLoad" = "Carga del Sistema" "systemLoadDesc" = "promedio de carga del sistema en los últimos 1, 5 y 15 minutos" "connectionCount" = "Número de Conexiones" "ipAddresses" = "Direcciones IP" "toggleIpVisibility" = "Alternar visibilidad de la IP" "overallSpeed" = "Velocidad general" "upload" = "Subida" "download" = "Descarga" "totalData" = "Datos totales" "sent" = "Enviado" "received" = "Recibido" "documentation" = "Documentación" "xraySwitchVersionDialog" = "¿Realmente deseas cambiar la versión de Xray?" "xraySwitchVersionDialogDesc" = "Esto cambiará la versión de Xray a #version#." "xraySwitchVersionPopover" = "Xray se actualizó correctamente" "geofileUpdateDialog" = "¿Realmente deseas actualizar el geofichero?" "geofileUpdateDialogDesc" = "Esto actualizará el archivo #filename#." "geofilesUpdateDialogDesc" = "Esto actualizará todos los archivos." "geofilesUpdateAll" = "Actualizar todo" "geofileUpdatePopover" = "Geofichero actualizado correctamente" "dontRefresh" = "La instalación está en progreso, por favor no actualices esta página." "logs" = "Registros" "config" = "Configuración" "backup" = "Сopia de Seguridad" "backupTitle" = "Copia de Seguridad y Restauración de la Base de Datos" "exportDatabase" = "Copia de seguridad" "exportDatabaseDesc" = "Haz clic para descargar un archivo .db que contiene una copia de seguridad de tu base de datos actual en tu dispositivo." "importDatabase" = "Restaurar" "importDatabaseDesc" = "Haz clic para seleccionar y cargar un archivo .db desde tu dispositivo para restaurar tu base de datos desde una copia de seguridad." "importDatabaseSuccess" = "La base de datos se ha importado correctamente" "importDatabaseError" = "Ocurrió un error al importar la base de datos" "readDatabaseError" = "Ocurrió un error al leer la base de datos" "getDatabaseError" = "Ocurrió un error al obtener la base de datos" "getConfigError" = "Ocurrió un error al obtener el archivo de configuración" [pages.inbounds] "allTimeTraffic" = "Tráfico Total" "allTimeTrafficUsage" = "Uso de datos histórico" "title" = "Entradas" "totalDownUp" = "Subidas/Descargas Totales" "totalUsage" = "Uso Total" "inboundCount" = "Número de Entradas" "operate" = "Menú" "enable" = "Habilitar" "remark" = "Notas" "protocol" = "Protocolo" "port" = "Puerto" "portMap" = "Puertos de Destino" "traffic" = "Tráfico" "details" = "Detalles" "transportConfig" = "Transporte" "expireDate" = "Fecha de Expiración" "createdAt" = "Creado" "updatedAt" = "Actualizado" "resetTraffic" = "Restablecer Tráfico" "addInbound" = "Agregar Entrada" "generalActions" = "Acciones Generales" "autoRefresh" = "Auto-actualizar" "autoRefreshInterval" = "Intervalo" "modifyInbound" = "Modificar Entrada" "deleteInbound" = "Eliminar Entrada" "deleteInboundContent" = "¿Confirmar eliminación de entrada?" "deleteClient" = "Eliminar cliente" "deleteClientContent" = "¿Está seguro de que desea eliminar el cliente?" "resetTrafficContent" = "¿Confirmar restablecimiento de tráfico?" "copyLink" = "Copiar Enlace" "address" = "Dirección" "network" = "Red" "destinationPort" = "Puerto de Destino" "targetAddress" = "Dirección de Destino" "monitorDesc" = "Dejar en blanco por defecto" "meansNoLimit" = " = illimitata. (unidad: GB)" "totalFlow" = "Flujo Total" "leaveBlankToNeverExpire" = "Dejar en Blanco para Nunca Expirar" "noRecommendKeepDefault" = "No hay requisitos especiales para mantener la configuración predeterminada" "certificatePath" = "Ruta Cert" "certificateContent" = "Datos Cert" "publicKey" = "Clave Pública" "privatekey" = "Clave Privada" "clickOnQRcode" = "Haz clic en el Código QR para Copiar" "client" = "Cliente" "export" = "Exportar Enlaces" "clone" = "Clonar" "cloneInbound" = "Clonar Entradas" "cloneInboundContent" = "Se aplicarán todas las configuraciones de esta entrada, excepto el Puerto, la IP de Escucha y los Clientes, al clon." "cloneInboundOk" = "Clonar" "resetAllTraffic" = "Restablecer Tráfico de Todas las Entradas" "resetAllTrafficTitle" = "Restablecer tráfico de todas las entradas" "resetAllTrafficContent" = "¿Estás seguro de que deseas restablecer el tráfico de todas las entradas?" "resetInboundClientTraffics" = "Restablecer Tráfico de Clientes" "resetInboundClientTrafficTitle" = "Restablecer todo el tráfico de clientes" "resetInboundClientTrafficContent" = "¿Estás seguro de que deseas restablecer todo el tráfico para los clientes de esta entrada?" "resetAllClientTraffics" = "Restablecer Tráfico de Todos los Clientes" "resetAllClientTrafficTitle" = "Restablecer todo el tráfico de clientes" "resetAllClientTrafficContent" = "¿Estás seguro de que deseas restablecer todo el tráfico para todos los clientes?" "delDepletedClients" = "Eliminar Clientes Agotados" "delDepletedClientsTitle" = "Eliminar clientes agotados" "delDepletedClientsContent" = "¿Estás seguro de que deseas eliminar todos los clientes agotados?" "email" = "Email" "emailDesc" = "Por favor proporciona una dirección de correo electrónico única." "IPLimit" = "Límite de IP" "IPLimitDesc" = "Desactiva la entrada si la cantidad supera el valor ingresado (ingresa 0 para desactivar el límite de IP)." "IPLimitlog" = "Registro de IP" "IPLimitlogDesc" = "Registro de historial de IPs (antes de habilitar la entrada después de que haya sido desactivada por el límite de IP, debes borrar el registro)." "IPLimitlogclear" = "Limpiar el Registro" "setDefaultCert" = "Establecer certificado desde el panel" "telegramDesc" = "Por favor, proporciona el ID de Chat de Telegram. (usa el comando '/id' en el bot) o (@userinfobot)" "subscriptionDesc" = "Puedes encontrar tu enlace de suscripción en Detalles, también puedes usar el mismo nombre para varias configuraciones." "info" = "Info" "same" = "misma" "inboundData" = "Datos de entrada" "exportInbound" = "Exportación entrante" "import" = "Importar" "importInbound" = "Importar un entrante" "periodicTrafficResetTitle" = "Reset de Tráfico" "periodicTrafficResetDesc" = "Reiniciar automáticamente el contador de tráfico en intervalos especificados" "lastReset" = "Último reinicio" [pages.client] "add" = "Agregar Cliente" "edit" = "Editar Cliente" "submitAdd" = "Agregar Cliente" "submitEdit" = "Guardar Cambios" "clientCount" = "Número de Clientes" "bulk" = "Agregar en Lote" "method" = "Método" "first" = "Primero" "last" = "Último" "prefix" = "Prefijo" "postfix" = "Sufijo" "delayedStart" = "Iniciar después del primer uso" "expireDays" = "Duración" "days" = "Día(s)" "renew" = "Renovación automática" "renewDesc" = "Renovación automática después de la expiración. (0 = desactivar) (unidad: día)" [pages.inbounds.periodicTrafficReset] "never" = "Nunca" "daily" = "Diariamente" "weekly" = "Semanalmente" "monthly" = "Mensualmente" [pages.inbounds.toasts] "obtain" = "Recibir" "updateSuccess" = "La actualización fue exitosa" "logCleanSuccess" = "El registro ha sido limpiado" "inboundsUpdateSuccess" = "Entradas actualizadas correctamente" "inboundUpdateSuccess" = "Entrada actualizada correctamente" "inboundCreateSuccess" = "Entrada creada correctamente" "inboundDeleteSuccess" = "Entrada eliminada correctamente" "inboundClientAddSuccess" = "Cliente(s) de entrada añadido(s)" "inboundClientDeleteSuccess" = "Cliente de entrada eliminado" "inboundClientUpdateSuccess" = "Cliente de entrada actualizado" "delDepletedClientsSuccess" = "Todos los clientes con tráfico agotado fueron eliminados" "resetAllClientTrafficSuccess" = "Todo el tráfico del cliente ha sido reiniciado" "resetAllTrafficSuccess" = "Todo el tráfico ha sido reiniciado" "resetInboundClientTrafficSuccess" = "El tráfico ha sido reiniciado" "trafficGetError" = "Error al obtener los tráficos" "getNewX25519CertError" = "Error al obtener el certificado X25519." "getNewmldsa65Error" = "Error al obtener el certificado mldsa65." "getNewVlessEncError" = "Error al obtener el certificado VlessEnc." [pages.inbounds.stream.general] "request" = "Pedido" "response" = "Respuesta" "name" = "Nombre" "value" = "Valor" [pages.inbounds.stream.tcp] "version" = "Versión" "method" = "Método" "path" = "Camino" "status" = "Estado" "statusDescription" = "Descripción de la Situación" "requestHeader" = "Encabezado de solicitud" "responseHeader" = "Encabezado de respuesta" [pages.settings] "title" = "Configuraciones" "save" = "Guardar" "infoDesc" = "Cada cambio realizado aquí debe ser guardado. Por favor, reinicie el panel para aplicar los cambios." "restartPanel" = "Reiniciar Panel" "restartPanelDesc" = "¿Está seguro de que desea reiniciar el panel? Haga clic en Aceptar para reiniciar después de 3 segundos. Si no puede acceder al panel después de reiniciar, por favor, consulte la información de registro del panel en el servidor." "restartPanelSuccess" = "El panel se reinició correctamente" "actions" = "Acciones" "resetDefaultConfig" = "Restablecer a Configuración Predeterminada" "panelSettings" = "Configuraciones del Panel" "securitySettings" = "Configuraciones de Seguridad" "TGBotSettings" = "Configuraciones de Bot de Telegram" "panelListeningIP" = "IP de Escucha del Panel" "panelListeningIPDesc" = "Dejar en blanco por defecto para monitorear todas las IPs." "panelListeningDomain" = "Dominio de Escucha del Panel" "panelListeningDomainDesc" = "Dejar en blanco por defecto para monitorear todos los dominios e IPs." "panelPort" = "Puerto del Panel" "panelPortDesc" = "El puerto utilizado para mostrar este panel." "publicKeyPath" = "Ruta del Archivo de Clave Pública del Certificado del Panel" "publicKeyPathDesc" = "Complete con una ruta absoluta que comience con." "privateKeyPath" = "Ruta del Archivo de Clave Privada del Certificado del Panel" "privateKeyPathDesc" = "Complete con una ruta absoluta que comience con." "panelUrlPath" = "Ruta Raíz de la URL del Panel" "panelUrlPathDesc" = "Debe empezar con '/' y terminar con." "pageSize" = "Tamaño de paginación" "pageSizeDesc" = "Defina el tamaño de página para la tabla de entradas. Establezca 0 para desactivar" "remarkModel" = "Modelo de observación y carácter de separación" "datepicker" = "selector de fechas" "datepickerPlaceholder" = "Seleccionar fecha" "datepickerDescription" = "El tipo de calendario selector especifica la fecha de vencimiento" "sampleRemark" = "Observación de muestra" "oldUsername" = "Nombre de Usuario Actual" "currentPassword" = "Contraseña Actual" "newUsername" = "Nuevo Nombre de Usuario" "newPassword" = "Nueva Contraseña" "telegramBotEnable" = "Habilitar bot de Telegram" "telegramBotEnableDesc" = "Conéctese a las funciones de este panel a través del bot de Telegram." "telegramToken" = "Token de Telegram" "telegramTokenDesc" = "Debe obtener el token del administrador de bots de Telegram @botfather." "telegramProxy" = "Socks5 Proxy" "telegramProxyDesc" = "Si necesita el proxy Socks5 para conectarse a Telegram. Ajuste su configuración según la guía." "telegramAPIServer" = "API Server de Telegram" "telegramAPIServerDesc" = "El servidor API de Telegram a utilizar. Déjelo en blanco para utilizar el servidor predeterminado." "telegramChatId" = "IDs de Chat de Telegram para Administradores" "telegramChatIdDesc" = "IDs de Chat múltiples separados por comas. Use @userinfobot o use el comando '/id' en el bot para obtener sus IDs de Chat." "telegramNotifyTime" = "Hora de Notificación del Bot de Telegram" "telegramNotifyTimeDesc" = "Usar el formato de tiempo de Crontab." "tgNotifyBackup" = "Respaldo de Base de Datos" "tgNotifyBackupDesc" = "Incluir archivo de respaldo de base de datos con notificación de informe." "tgNotifyLogin" = "Notificación de Inicio de Sesión" "tgNotifyLoginDesc" = "Muestra el nombre de usuario, dirección IP y hora cuando alguien intenta iniciar sesión en su panel." "sessionMaxAge" = "Edad Máxima de Sesión" "sessionMaxAgeDesc" = "La duración de una sesión de inicio de sesión (unidad: minutos)." "expireTimeDiff" = "Umbral de Expiración para Notificación" "expireTimeDiffDesc" = "Reciba notificaciones sobre la expiración de la cuenta antes del umbral (unidad: días)." "trafficDiff" = "Umbral de Tráfico para Notificación" "trafficDiffDesc" = "Reciba notificaciones sobre el agotamiento del tráfico antes de alcanzar el umbral (unidad: GB)." "tgNotifyCpu" = "Umbral de Alerta de Porcentaje de CPU" "tgNotifyCpuDesc" = "Reciba notificaciones si el uso de la CPU supera este umbral (unidad: %)." "timeZone" = "Zona Horaria" "timeZoneDesc" = "Las tareas programadas se ejecutan de acuerdo con la hora en esta zona horaria." "subSettings" = "Suscripción" "subEnable" = "Habilitar Servicio" "subEnableDesc" = "Función de suscripción con configuración separada." "subJsonEnable" = "Habilitar/Deshabilitar el endpoint de suscripción JSON de forma independiente." "subTitle" = "Título de la Suscripción" "subTitleDesc" = "Título mostrado en el cliente VPN" "subSupportUrl" = "URL de soporte" "subSupportUrlDesc" = "Enlace de soporte técnico mostrado en el cliente VPN" "subProfileUrl" = "URL del perfil" "subProfileUrlDesc" = "Un enlace a tu sitio web mostrado en el cliente VPN" "subAnnounce" = "Anuncio" "subAnnounceDesc" = "El texto del anuncio mostrado en el cliente VPN" "subEnableRouting" = "Habilitar enrutamiento" "subEnableRoutingDesc" = "Configuración global para habilitar el enrutamiento en el cliente VPN. (Solo para Happ)" "subRoutingRules" = "Reglas de enrutamiento" "subRoutingRulesDesc" = "Reglas de enrutamiento globales para el cliente VPN. (Solo para Happ)" "subListen" = "Listening IP" "subListenDesc" = "Dejar en blanco por defecto para monitorear todas las IPs." "subPort" = "Puerto de Suscripción" "subPortDesc" = "El número de puerto para el servicio de suscripción debe estar sin usar en el servidor." "subCertPath" = "Ruta del Archivo de Clave Pública del Certificado de Suscripción" "subCertPathDesc" = "Complete con una ruta absoluta que comience con '/'" "subKeyPath" = "Ruta del Archivo de Clave Privada del Certificado de Suscripción" "subKeyPathDesc" = "Complete con una ruta absoluta que comience con '/'" "subPath" = "Ruta Raíz de la URL de Suscripción" "subPathDesc" = "Debe empezar con '/' y terminar con '/'" "subDomain" = "Dominio de Escucha" "subDomainDesc" = "Dejar en blanco por defecto para monitorear todos los dominios e IPs." "subUpdates" = "Intervalos de Actualización de Suscripción" "subUpdatesDesc" = "Horas de intervalo entre actualizaciones en la aplicación del cliente." "subEncrypt" = "Encriptar configuraciones" "subEncryptDesc" = "Encriptar las configuraciones devueltas en la suscripción." "subShowInfo" = "Mostrar información de uso" "subShowInfoDesc" = "Mostrar tráfico restante y fecha después del nombre de configuración." "subURI" = "URI de proxy inverso" "externalTrafficInformEnable" = "Informe de tráfico externo" "externalTrafficInformEnableDesc" = "Informar a la API externa sobre cada actualización de tráfico." "externalTrafficInformURI" = "URI de información de tráfico externo" "externalTrafficInformURIDesc" = "Las actualizaciones de tráfico se envían a este URI." "subURIDesc" = "Cambiar el URI base de la URL de suscripción para usar detrás de los servidores proxy" "fragment" = "Fragmentación" "fragmentDesc" = "Habilitar la fragmentación para el paquete de saludo de TLS" "fragmentSett" = "Configuración de Fragmentación" "noisesDesc" = "Activar Sonidos" "noisesSett" = "Configuración de Sonidos" "mux" = "Mux" "muxDesc" = "Transmite múltiples flujos de datos independientes dentro de un flujo de datos establecido." "muxSett" = "Configuración Mux" "direct" = "Conexión Directa" "directDesc" = "Establece conexiones directas con dominios o rangos de IP de un país específico." "notifications" = "Notificaciones" "certs" = "Certificados" "externalTraffic" = "Tráfico Externo" "dateAndTime" = "Fecha y Hora" "proxyAndServer" = "Proxy y Servidor" "intervals" = "Intervalos" "information" = "Información" "language" = "Idioma" "telegramBotLanguage" = "Idioma del Bot de Telegram" [pages.xray] "title" = "Xray Configuración" "save" = "Guardar configuración" "restart" = "Reiniciar Xray" "restartSuccess" = "Xray se ha reiniciado correctamente" "stopSuccess" = "Xray se ha detenido correctamente" "restartError" = "Ocurrió un error al reiniciar Xray." "stopError" = "Ocurrió un error al detener Xray." "basicTemplate" = "Perfil Básico" "advancedTemplate" = "Perfil Avanzado" "generalConfigs" = "Configuraciones Generales" "generalConfigsDesc" = "Estas opciones proporcionarán ajustes generales." "logConfigs" = "Registro" "logConfigsDesc" = "Los registros pueden afectar la eficiencia de su servidor. Se recomienda habilitarlos sabiamente solo en caso de sus necesidades." "blockConfigsDesc" = "Estas opciones evitarán que los usuarios se conecten a protocolos y sitios web específicos." "basicRouting" = "Enrutamiento Básico" "blockConnectionsConfigsDesc" = "Estas opciones bloquearán el tráfico según el país solicitado específico." "directConnectionsConfigsDesc" = "Una conexión directa asegura que el tráfico específico no sea enrutado a través de otro servidor." "blockips" = "Bloquear IPs" "blockdomains" = "Bloquear Dominios" "directips" = "IPs Directas" "directdomains" = "Dominios Directos" "ipv4Routing" = "Enrutamiento IPv4" "ipv4RoutingDesc" = "Estas opciones solo enrutarán a los dominios objetivo a través de IPv4." "warpRouting" = "Enrutamiento WARP" "warpRoutingDesc" = "Precaución: Antes de usar estas opciones, instale WARP en modo de proxy socks5 en su servidor siguiendo los pasos en el GitHub del panel. WARP enrutará el tráfico a los sitios web a través de los servidores de Cloudflare." "Template" = "Plantilla de Configuración de Xray" "TemplateDesc" = "Genera el archivo de configuración final de Xray basado en esta plantilla." "FreedomStrategy" = "Configurar Estrategia para el Protocolo Freedom" "FreedomStrategyDesc" = "Establece la estrategia de salida de la red en el Protocolo Freedom." "RoutingStrategy" = "Configurar Estrategia de Enrutamiento de Dominios" "RoutingStrategyDesc" = "Establece la estrategia general de enrutamiento para la resolución de DNS." "outboundTestUrl" = "URL de prueba de outbound" "outboundTestUrlDesc" = "URL usada al probar la conectividad del outbound" "Torrent" = "Prohibir Uso de BitTorrent" "Inbounds" = "Entrante" "InboundsDesc" = "Cambia la plantilla de configuración para aceptar clientes específicos." "Outbounds" = "Salidas" "Balancers" = "Equilibradores" "OutboundsDesc" = "Cambia la plantilla de configuración para definir formas de salida para este servidor." "Routings" = "Reglas de enrutamiento" "RoutingsDesc" = "¡La prioridad de cada regla es importante!" "completeTemplate" = "Todos" "logLevel" = "Nivel de registro" "logLevelDesc" = "El nivel de registro para registros de errores, que indica la información que debe registrarse." "accessLog" = "Registro de acceso" "accessLogDesc" = "La ruta del archivo para el registro de acceso. El valor especial 'ninguno' deshabilita los registros de acceso" "errorLog" = "Registro de Errores" "errorLogDesc" = "La ruta del archivo para el registro de errores. El valor especial 'none' desactiva los registros de errores." "dnsLog" = "Registro DNS" "dnsLogDesc" = "Si habilitar los registros de consulta DNS" "maskAddress" = "Enmascarar Dirección" "maskAddressDesc" = "Máscara de dirección IP, cuando se habilita, reemplazará automáticamente la dirección IP que aparece en el registro." "statistics" = "Estadísticas" "statsInboundUplink" = "Estadísticas de Subida de Entrada" "statsInboundUplinkDesc" = "Habilita la recopilación de estadísticas para el tráfico ascendente de todos los proxies de entrada." "statsInboundDownlink" = "Estadísticas de Bajada de Entrada" "statsInboundDownlinkDesc" = "Habilita la recopilación de estadísticas para el tráfico descendente de todos los proxies de entrada." "statsOutboundUplink" = "Estadísticas de Subida de Salida" "statsOutboundUplinkDesc" = "Habilita la recopilación de estadísticas para el tráfico ascendente de todos los proxies de salida." "statsOutboundDownlink" = "Estadísticas de Bajada de Salida" "statsOutboundDownlinkDesc" = "Habilita la recopilación de estadísticas para el tráfico descendente de todos los proxies de salida." [pages.xray.rules] "first" = "Primero" "last" = "Último" "up" = "Arriba" "down" = "Abajo" "source" = "Fuente" "dest" = "Destino" "inbound" = "Entrante" "outbound" = "Saliente" "balancer" = "Equilibrador" "info" = "Información" "add" = "Agregar Regla" "edit" = "Editar Regla" "useComma" = "Elementos separados por comas" [pages.xray.outbound] "addOutbound" = "Agregar salida" "addReverse" = "Agregar reverso" "editOutbound" = "Editar salida" "editReverse" = "Editar reverso" "tag" = "Etiqueta" "tagDesc" = "etiqueta única" "address" = "Dirección" "reverse" = "Reverso" "domain" = "Dominio" "type" = "Tipo" "bridge" = "puente" "portal" = "portal" "link" = "Enlace" "intercon" = "Interconexión" "settings" = "Configuración" "accountInfo" = "Información de la Cuenta" "outboundStatus" = "Estado de Salida" "sendThrough" = "Enviar a través de" "test" = "Probar" "testResult" = "Resultado de la prueba" "testing" = "Probando conexión..." "testSuccess" = "Prueba exitosa" "testFailed" = "Prueba fallida" "testError" = "Error al probar la salida" [pages.xray.balancer] "addBalancer" = "Agregar equilibrador" "editBalancer" = "Editar balanceador" "balancerStrategy" = "Estrategia" "balancerSelectors" = "Selectores" "tag" = "Etiqueta" "tagDesc" = "etiqueta única" "balancerDesc" = "No es posible utilizar balancerTag y outboundTag al mismo tiempo. Si se utilizan al mismo tiempo, sólo funcionará outboundTag." [pages.xray.wireguard] "secretKey" = "Llave secreta" "publicKey" = "Llave pública" "allowedIPs" = "IP permitidas" "endpoint" = "Punto final" "psk" = "Clave precompartida" "domainStrategy" = "Estrategia de dominio" [pages.xray.tun] "nameDesc" = "El nombre de la interfaz TUN. El valor predeterminado es 'xray0'" "mtuDesc" = "Unidad Máxima de Transmisión. El tamaño máximo de los paquetes de datos. El valor predeterminado es 1500" "userLevel" = "Nivel de Usuario" "userLevelDesc" = "Todas las conexiones realizadas a través de este entrada utilizarán este nivel de usuario. El valor predeterminado es 0" [pages.xray.dns] "enable" = "Habilitar DNS" "enableDesc" = "Habilitar servidor DNS incorporado" "tag" = "Etiqueta de Entrada DNS" "tagDesc" = "Esta etiqueta estará disponible como una etiqueta de entrada en las reglas de enrutamiento." "clientIp" = "IP del cliente" "clientIpDesc" = "Se utiliza para notificar al servidor la ubicación IP especificada durante las consultas DNS" "disableCache" = "Desactivar caché" "disableCacheDesc" = "Desactiva el almacenamiento en caché de DNS" "disableFallback" = "Desactivar respaldo" "disableFallbackDesc" = "Desactiva las consultas DNS de respaldo" "disableFallbackIfMatch" = "Desactivar respaldo si coincide" "disableFallbackIfMatchDesc" = "Desactiva las consultas DNS de respaldo cuando se acierta en la lista de dominios coincidentes del servidor DNS" "enableParallelQuery" = "Habilitar consulta paralela" "enableParallelQueryDesc" = "Habilitar consultas DNS paralelas a múltiples servidores para una resolución más rápida" "strategy" = "Estrategia de Consulta" "strategyDesc" = "Estrategia general para resolver nombres de dominio" "add" = "Agregar Servidor" "edit" = "Editar Servidor" "domains" = "Dominios" "expectIPs" = "IPs esperadas" "unexpectIPs" = "IPs inesperadas" "useSystemHosts" = "Usar Hosts del sistema" "useSystemHostsDesc" = "Usar el archivo hosts de un sistema instalado" "usePreset" = "Usar plantilla" "dnsPresetTitle" = "Plantillas DNS" "dnsPresetFamily" = "Familiar" [pages.xray.fakedns] "add" = "Agregar DNS Falso" "edit" = "Editar DNS Falso" "ipPool" = "Subred del grupo de IP" "poolSize" = "Tamaño del grupo" [pages.settings.security] "admin" = "Credenciales de administrador" "twoFactor" = "Autenticación de dos factores" "twoFactorEnable" = "Habilitar 2FA" "twoFactorEnableDesc" = "Añade una capa adicional de autenticación para mayor seguridad." "twoFactorModalSetTitle" = "Activar autenticación de dos factores" "twoFactorModalDeleteTitle" = "Desactivar autenticación de dos factores" "twoFactorModalSteps" = "Para configurar la autenticación de dos factores, sigue estos pasos:" "twoFactorModalFirstStep" = "1. Escanea este código QR en la aplicación de autenticación o copia el token cerca del código QR y pégalo en la aplicación" "twoFactorModalSecondStep" = "2. Ingresa el código de la aplicación" "twoFactorModalRemoveStep" = "Ingresa el código de la aplicación para eliminar la autenticación de dos factores." "twoFactorModalChangeCredentialsTitle" = "Cambiar credenciales" "twoFactorModalChangeCredentialsStep" = "Ingrese el código de la aplicación para cambiar las credenciales del administrador." "twoFactorModalSetSuccess" = "La autenticación de dos factores se ha establecido con éxito" "twoFactorModalDeleteSuccess" = "La autenticación de dos factores se ha eliminado con éxito" "twoFactorModalError" = "Código incorrecto" [pages.settings.toasts] "modifySettings" = "Los parámetros han sido modificados." "getSettings" = "Ocurrió un error al obtener los parámetros." "modifyUserError" = "Ocurrió un error al cambiar las credenciales del administrador." "modifyUser" = "Has cambiado exitosamente las credenciales del administrador." "originalUserPassIncorrect" = "Nombre de usuario o contraseña original incorrectos" "userPassMustBeNotEmpty" = "El nuevo nombre de usuario y la nueva contraseña no pueden estar vacíos" "getOutboundTrafficError" = "Error al obtener el tráfico saliente" "resetOutboundTrafficError" = "Error al reiniciar el tráfico saliente" [tgbot] "keyboardClosed" = "❌ Teclado cerrado!" "noResult" = "❗ ¡Sin resultados!" "noQuery" = "❌ ¡Consulta no encontrada! ¡Por favor, use el comando nuevamente!" "wentWrong" = "❌ ¡Algo salió mal!" "noIpRecord" = "❗ ¡No hay registro de IP!" "noInbounds" = "❗ ¡No se encontraron entradas!" "unlimited" = "♾ Ilimitado (Restablecer)" "add" = "Añadir" "month" = "Mes" "months" = "Meses" "day" = "Día" "days" = "Días" "hours" = "Horas" "minutes" = "Minutos" "unknown" = "Desconocido" "inbounds" = "Entradas" "clients" = "Clientes" "offline" = "🔴 Desconectado" "online" = "🟢 En línea" [tgbot.commands] "unknown" = "❗ Comando desconocido" "pleaseChoose" = "👇 Por favor elige:\r\n" "help" = "🤖 ¡Bienvenido a este bot! Está diseñado para ofrecerte datos específicos del servidor y te permite hacer modificaciones según sea necesario.\r\n\r\n" "start" = "👋 Hola {{ .Firstname }}.\r\n" "welcome" = "🤖 Bienvenido al bot de gestión de {{ .Hostname }}.\r\n" "status" = "✅ ¡El bot está bien!" "usage" = "❗ ¡Por favor proporciona un texto para buscar!" "getID" = "🆔 Tu ID: {{ .ID }}" "helpAdminCommands" = "Para reiniciar Xray Core:\r\n/restart\r\n\r\nPara buscar un correo electrónico de cliente:\r\n/usage [Correo electrónico]\r\n\r\nPara buscar entradas (con estadísticas de cliente):\r\n/inbound [Observación]\r\n\r\nID de Chat de Telegram:\r\n/id" "helpClientCommands" = "Para buscar estadísticas, utiliza el siguiente comando:\r\n/usage [Correo electrónico]\r\n\r\nID de Chat de Telegram:\r\n/id" "restartUsage" = "\r\n\r\n/restart" "restartSuccess" = "✅ ¡Operación exitosa!" "restartFailed" = "❗ Error en la operación.\r\n\r\nError: {{ .Error }}." "xrayNotRunning" = "❗ Xray Core no está en ejecución." "startDesc" = "Mostrar el menú principal" "helpDesc" = "Ayuda del bot" "statusDesc" = "Comprobar el estado del bot" "idDesc" = "Mostrar tu ID de Telegram" [tgbot.messages] "cpuThreshold" = "🔴 El uso de CPU {{ .Percent }}% es mayor que el umbral {{ .Threshold }}%" "selectUserFailed" = "❌ ¡Error al seleccionar usuario!" "userSaved" = "✅ Usuario de Telegram guardado." "loginSuccess" = "✅ Has iniciado sesión en el panel con éxito.\r\n" "loginFailed" = "❗️ Falló el inicio de sesión en el panel.\r\n" "2faFailed" = "Error de 2FA" "report" = "🕰 Informes programados: {{ .RunTime }}\r\n" "datetime" = "⏰ Fecha y Hora: {{ .DateTime }}\r\n" "hostname" = "💻 Nombre del Host: {{ .Hostname }}\r\n" "version" = "🚀 Versión de X-UI: {{ .Version }}\r\n" "xrayVersion" = "📡 Versión de Xray: {{ .XrayVersion }}\r\n" "ipv6" = "🌐 IPv6: {{ .IPv6 }}\r\n" "ipv4" = "🌐 IPv4: {{ .IPv4 }}\r\n" "ip" = "🌐 IP: {{ .IP }}\r\n" "ips" = "🔢 IPs:\r\n{{ .IPs }}\r\n" "serverUpTime" = "⏳ Tiempo de actividad del servidor: {{ .UpTime }} {{ .Unit }}\r\n" "serverLoad" = "📈 Carga del servidor: {{ .Load1 }}, {{ .Load2 }}, {{ .Load3 }}\r\n" "serverMemory" = "📋 Memoria del servidor: {{ .Current }}/{{ .Total }}\r\n" "tcpCount" = "🔹 Conteo de TCP: {{ .Count }}\r\n" "udpCount" = "🔸 Conteo de UDP: {{ .Count }}\r\n" "traffic" = "🚦 Tráfico: {{ .Total }} (↑{{ .Upload }},↓{{ .Download }})\r\n" "xrayStatus" = "ℹ️ Estado de Xray: {{ .State }}\r\n" "username" = "👤 Nombre de usuario: {{ .Username }}\r\n" "password" = "👤 Contraseña: {{ .Password }}\r\n" "time" = "⏰ Hora: {{ .Time }}\r\n" "inbound" = "📍 Inbound: {{ .Remark }}\r\n" "port" = "🔌 Puerto: {{ .Port }}\r\n" "expire" = "📅 Fecha de Vencimiento: {{ .Time }}\r\n" "expireIn" = "📅 Vence en: {{ .Time }}\r\n" "active" = "💡 Activo: {{ .Enable }}\r\n" "enabled" = "🚨 Habilitado: {{ .Enable }}\r\n" "online" = "🌐 Estado de conexión: {{ .Status }}\r\n" "lastOnline" = "🔙 Última conexión: {{ .Time }}\r\n" "email" = "📧 Email: {{ .Email }}\r\n" "upload" = "🔼 Subida: ↑{{ .Upload }}\r\n" "download" = "🔽 Bajada: ↓{{ .Download }}\r\n" "total" = "📊 Total: ↑↓{{ .UpDown }} / {{ .Total }}\r\n" "TGUser" = "👤 Usuario de Telegram: {{ .TelegramID }}\r\n" "exhaustedMsg" = "🚨 Agotado {{ .Type }}:\r\n" "exhaustedCount" = "🚨 Cantidad de Agotados {{ .Type }}:\r\n" "onlinesCount" = "🌐 Clientes en línea: {{ .Count }}\r\n" "disabled" = "🛑 Desactivado: {{ .Disabled }}\r\n" "depleteSoon" = "🔜 Se agotará pronto: {{ .Deplete }}\r\n\r\n" "backupTime" = "🗄 Hora de la Copia de Seguridad: {{ .Time }}\r\n" "refreshedOn" = "\r\n📋🔄 Actualizado en: {{ .Time }}\r\n\r\n" "yes" = "✅ Sí" "no" = "❌ No" "received_id" = "🔑📥 ID actualizado." "received_password" = "🔑📥 Contraseña actualizada." "received_email" = "📧📥 Correo electrónico actualizado." "received_comment" = "💬📥 Comentario actualizado." "id_prompt" = "🔑 ID predeterminado: {{ .ClientId }}\n\nIntroduce tu ID." "pass_prompt" = "🔑 Contraseña predeterminada: {{ .ClientPassword }}\n\nIntroduce tu contraseña." "email_prompt" = "📧 Correo electrónico predeterminado: {{ .ClientEmail }}\n\nIntroduce tu correo electrónico." "comment_prompt" = "💬 Comentario predeterminado: {{ .ClientComment }}\n\nIntroduce tu comentario." "inbound_client_data_id" = "🔄 Entrada: {{ .InboundRemark }}\n\n🔑 ID: {{ .ClientId }}\n📧 Correo: {{ .ClientEmail }}\n📊 Tráfico: {{ .ClientTraffic }}\n📅 Fecha de expiración: {{ .ClientExp }}\n🌐 Límite de IP: {{ .IpLimit }}\n💬 Comentario: {{ .ClientComment }}\n\n¡Ahora puedes agregar al cliente a la entrada!" "inbound_client_data_pass" = "🔄 Entrada: {{ .InboundRemark }}\n\n🔑 Contraseña: {{ .ClientPass }}\n📧 Correo: {{ .ClientEmail }}\n📊 Tráfico: {{ .ClientTraffic }}\n📅 Fecha de expiración: {{ .ClientExp }}\n🌐 Límite de IP: {{ .IpLimit }}\n💬 Comentario: {{ .ClientComment }}\n\n¡Ahora puedes agregar al cliente a la entrada!" "cancel" = "❌ ¡Proceso cancelado! \n\nPuedes /start de nuevo en cualquier momento. 🔄" "error_add_client" = "⚠️ Error:\n\n {{ .error }}" "using_default_value" = "Está bien, me quedaré con el valor predeterminado. 😊" "incorrect_input" = "Tu entrada no es válida.\nLas frases deben ser continuas sin espacios.\nEjemplo correcto: aaaaaa\nEjemplo incorrecto: aaa aaa 🚫" "AreYouSure" = "¿Estás seguro? 🤔" "SuccessResetTraffic" = "📧 Correo: {{ .ClientEmail }}\n🏁 Resultado: ✅ Éxito" "FailedResetTraffic" = "📧 Correo: {{ .ClientEmail }}\n🏁 Resultado: ❌ Fallido \n\n🛠️ Error: [ {{ .ErrorMessage }} ]" "FinishProcess" = "🔚 Proceso de reinicio de tráfico finalizado para todos los clientes." [tgbot.buttons] "closeKeyboard" = "❌ Cerrar Teclado" "cancel" = "❌ Cancelar" "cancelReset" = "❌ Cancelar Reinicio" "cancelIpLimit" = "❌ Cancelar Límite de IP" "confirmResetTraffic" = "✅ ¿Confirmar Reinicio de Tráfico?" "confirmClearIps" = "✅ ¿Confirmar Limpiar IPs?" "confirmRemoveTGUser" = "✅ ¿Confirmar Eliminar Usuario de Telegram?" "confirmToggle" = "✅ ¿Confirmar habilitar/deshabilitar usuario?" "dbBackup" = "Obtener Copia de Seguridad de BD" "serverUsage" = "Uso del Servidor" "getInbounds" = "Obtener Entradas" "depleteSoon" = "Pronto se Agotará" "clientUsage" = "Obtener Uso" "onlines" = "Clientes en línea" "commands" = "Comandos" "refresh" = "🔄 Actualizar" "clearIPs" = "❌ Limpiar IPs" "removeTGUser" = "❌ Eliminar Usuario de Telegram" "selectTGUser" = "👤 Seleccionar Usuario de Telegram" "selectOneTGUser" = "👤 Selecciona un usuario de telegram:" "resetTraffic" = "📈 Reiniciar Tráfico" "resetExpire" = "📅 Cambiar fecha de Vencimiento" "ipLog" = "🔢 Registro de IP" "ipLimit" = "🔢 Límite de IP" "setTGUser" = "👤 Establecer Usuario de Telegram" "toggle" = "🔘 Habilitar / Deshabilitar" "custom" = "🔢 Costumbre" "confirmNumber" = "✅ Confirmar: {{ .Num }}" "confirmNumberAdd" = "✅ Confirmar agregando: {{ .Num }}" "limitTraffic" = "🚧 Límite de tráfico" "getBanLogs" = "Registros de prohibición" "allClients" = "Todos los Clientes" "addClient" = "Añadir cliente" "submitDisable" = "Enviar como deshabilitado ☑️" "submitEnable" = "Enviar como habilitado ✅" "use_default" = "🏷️ Usar por defecto" "change_id" = "⚙️🔑 ID" "change_password" = "⚙️🔑 Contraseña" "change_email" = "⚙️📧 Correo electrónico" "change_comment" = "⚙️💬 Comentario" "ResetAllTraffics" = "Reiniciar todo el tráfico" "SortedTrafficUsageReport" = "Informe de uso de tráfico ordenado" [tgbot.answers] "successfulOperation" = "✅ ¡Exitosa!" "errorOperation" = "❗ Error en la Operación." "getInboundsFailed" = "❌ Error al obtener las entradas" "getClientsFailed" = "❌ No se pudo obtener los clientes." "canceled" = "❌ {{ .Email }} : Operación cancelada." "clientRefreshSuccess" = "✅ {{ .Email }} : Cliente actualizado exitosamente." "IpRefreshSuccess" = "✅ {{ .Email }} : IPs actualizadas exitosamente." "TGIdRefreshSuccess" = "✅ {{ .Email }} : Usuario de Telegram del cliente actualizado exitosamente." "resetTrafficSuccess" = "✅ {{ .Email }} : Tráfico reiniciado exitosamente." "setTrafficLimitSuccess" = "✅ {{ .Email }} : Límite de Tráfico guardado exitosamente." "expireResetSuccess" = "✅ {{ .Email }} : Días de vencimiento reiniciados exitosamente." "resetIpSuccess" = "✅ {{ .Email }} : Límite de IP {{ .Count }} guardado exitosamente." "clearIpSuccess" = "✅ {{ .Email }} : IPs limpiadas exitosamente." "getIpLog" = "✅ {{ .Email }} : Obtener Registro de IP." "getUserInfo" = "✅ {{ .Email }} : Obtener Información de Usuario de Telegram." "removedTGUserSuccess" = "✅ {{ .Email }} : Usuario de Telegram eliminado exitosamente." "enableSuccess" = "✅ {{ .Email }} : Habilitado exitosamente." "disableSuccess" = "✅ {{ .Email }} : Deshabilitado exitosamente." "askToAddUserId" = "¡No se encuentra su configuración!\r\nPor favor, pídale a su administrador que use su ChatID de usuario de Telegram en su(s) configuración(es).\r\n\r\nSu ChatID de usuario: {{ .TgUserID }}" "chooseClient" = "Elige un Cliente para Inbound {{ .Inbound }}" "chooseInbound" = "Elige un Inbound" ================================================ FILE: web/translation/translate.fa_IR.toml ================================================ "username" = "نام‌کاربری" "password" = "رمزعبور" "login" = "ورود" "confirm" = "تایید" "cancel" = "انصراف" "close" = "بستن" "create" = "ایجاد" "update" = "به‌روزرسانی" "copy" = "کپی" "copied" = "کپی شد" "download" = "دانلود" "remark" = "نام" "enable" = "فعال" "protocol" = "پروتکل" "search" = "جستجو" "filter" = "فیلتر" "loading" = "...در حال بارگذاری" "second" = "ثانیه" "minute" = "دقیقه" "hour" = "ساعت" "day" = "روز" "check" = "چک کردن" "indefinite" = "نامحدود" "unlimited" = "نامحدود" "none" = "هیچ" "qrCode" = "QRکد" "info" = "اطلاعات بیشتر" "edit" = "ویرایش" "delete" = "حذف" "reset" = "ریست" "noData" = "داده‌ای وجود ندارد." "copySuccess" = "باموفقیت کپی‌شد" "sure" = "مطمئن" "encryption" = "رمزگذاری" "useIPv4ForHost" = "از IPv4 برای میزبان استفاده کنید" "transmission" = "راه‌اتصال" "host" = "آدرس" "path" = "مسیر" "camouflage" = "مبهم‌سازی" "status" = "وضعیت" "enabled" = "فعال" "disabled" = "غیرفعال" "depleted" = "منقضی" "depletingSoon" = "در‌حال‌انقضا" "offline" = "آفلاین" "online" = "آنلاین" "domainName" = "آدرس دامنه" "monitor" = "آی‌پی اتصال" "certificate" = "گواهی دیجیتال" "fail" = "ناموفق" "comment" = "توضیحات" "success" = "موفق" "lastOnline" = "آخرین فعالیت" "getVersion" = "دریافت نسخه" "install" = "نصب" "clients" = "کاربران" "usage" = "استفاده" "twoFactorCode" = "کد" "remained" = "باقی‌مانده" "security" = "امنیت" "secAlertTitle" = "هشدار‌امنیتی" "secAlertSsl" = "این‌اتصال‌امن نیست. لطفا‌ تازمانی‌که تی‌ال‌اس برای محافظت از‌ داده‌ها فعال نشده‌است، از وارد کردن اطلاعات حساس خودداری کنید" "secAlertConf" = "تنظیمات خاصی در برابر حملات آسیب پذیر هستند. توصیه می‌شود پروتکل‌های امنیتی را برای جلوگیری از نفوذ احتمالی تقویت کنید" "secAlertSSL" = "پنل فاقد ارتباط امن است. لطفاً یک گواهینامه تی‌ال‌اس برای محافظت از داده‌ها نصب کنید" "secAlertPanelPort" = "استفاده از پورت پیش‌فرض پنل ناامن است. لطفاً یک پورت تصادفی یا خاص تنظیم کنید" "secAlertPanelURI" = "مسیر پیش‌فرض لینک پنل ناامن است. لطفاً یک مسیر پیچیده تنظیم کنید" "secAlertSubURI" = "مسیر پیش‌فرض لینک سابسکریپشن ناامن است. لطفاً یک مسیر پیچیده تنظیم کنید" "secAlertSubJsonURI" = "مسیر پیش‌فرض لینک سابسکریپشن جیسون ناامن است. لطفاً یک مسیر پیچیده تنظیم کنید" "emptyDnsDesc" = "هیچ سرور DNS اضافه نشده است." "emptyFakeDnsDesc" = "هیچ سرور Fake DNS اضافه نشده است." "emptyBalancersDesc" = "هیچ بالانسر اضافه نشده است." "emptyReverseDesc" = "هیچ پروکسی معکوس اضافه نشده است." "somethingWentWrong" = "مشکلی پیش آمد" [subscription] "title" = "اطلاعات سابسکریپشن" "subId" = "شناسه اشتراک" "status" = "وضعیت" "downloaded" = "دانلود" "uploaded" = "آپلود" "expiry" = "تاریخ پایان" "totalQuota" = "حجم کلی" "individualLinks" = "لینک‌های تکی" "active" = "فعال" "inactive" = "غیرفعال" "unlimited" = "نامحدود" "noExpiry" = "بدون انقضا" [menu] "theme" = "تم" "dark" = "تیره" "ultraDark" = "فوق تیره" "dashboard" = "نمای کلی" "inbounds" = "ورودی‌ها" "settings" = "تنظیمات پنل" "xray" = "پیکربندی ایکس‌ری" "logout" = "خروج" "link" = "مدیریت" [pages.login] "hello" = "سلام" "title" = "خوش‌آمدید" "loginAgain" = "مدت زمان استفاده به‌اتمام‌رسیده، لطفا دوباره وارد شوید" [pages.login.toasts] "invalidFormData" = "اطلاعات به‌درستی وارد نشده‌است" "emptyUsername" = "لطفا یک نام‌کاربری وارد کنید‌" "emptyPassword" = "لطفا یک رمزعبور وارد کنید" "wrongUsernameOrPassword" = "نام کاربری، رمز عبور یا کد دو مرحله‌ای نامعتبر است." "successLogin" = "شما با موفقیت به حساب کاربری خود وارد شدید." [pages.index] "title" = "نمای کلی" "cpu" = "پردازنده" "logicalProcessors" = "پردازنده‌های منطقی" "frequency" = "فرکانس" "swap" = "سواپ" "storage" = "ذخیره‌سازی" "memory" = "حافظه رم" "threads" = "رشته‌ها" "xrayStatus" = "ایکس‌ری" "stopXray" = "توقف" "restartXray" = "شروع‌مجدد" "xraySwitch" = "‌نسخه" "xraySwitchClick" = "نسخه مورد نظر را انتخاب کنید" "xraySwitchClickDesk" = "لطفا بادقت انتخاب کنید. درصورت انتخاب نسخه قدیمی‌تر، امکان ناهماهنگی با پیکربندی فعلی وجود دارد" "xrayStatusUnknown" = "ناشناخته" "xrayStatusRunning" = "در حال اجرا" "xrayStatusStop" = "متوقف" "xrayStatusError" = "خطا" "xrayErrorPopoverTitle" = "خطا در هنگام اجرای Xray رخ داد" "operationHours" = "مدت‌کارکرد" "systemLoad" = "بارسیستم" "systemLoadDesc" = "میانگین بار سیستم برای 1، 5 و 15 دقیقه گذشته" "connectionCount" = "تعداد کانکشن ها" "ipAddresses" = "آدرس‌های IP" "toggleIpVisibility" = "تغییر وضعیت نمایش IP" "overallSpeed" = "سرعت کلی" "upload" = "آپلود" "download" = "دانلود" "totalData" = "داده‌های کل" "sent" = "ارسال شده" "received" = "دریافت شده" "documentation" = "مستندات" "xraySwitchVersionDialog" = "آیا واقعاً می‌خواهید نسخه Xray را تغییر دهید؟" "xraySwitchVersionDialogDesc" = "این کار نسخه Xray را به #version# تغییر می‌دهد." "xraySwitchVersionPopover" = "Xray با موفقیت به‌روز شد" "geofileUpdateDialog" = "آیا واقعاً می‌خواهید فایل جغرافیایی را به‌روز کنید؟" "geofileUpdateDialogDesc" = "این عمل فایل #filename# را به‌روز می‌کند." "geofilesUpdateDialogDesc" = "با این کار همه فایل‌ها به‌روزرسانی می‌شوند." "geofilesUpdateAll" = "همه را به‌روزرسانی کنید" "geofileUpdatePopover" = "فایل جغرافیایی با موفقیت به‌روز شد" "dontRefresh" = "در حال نصب، لطفا صفحه را رفرش نکنید" "logs" = "گزارش‌ها" "config" = "پیکربندی" "backup" = "پشتیبان‌گیری" "backupTitle" = "پشتیبان‌گیری دیتابیس" "exportDatabase" = "پشتیبان‌گیری" "exportDatabaseDesc" = "برای دانلود یک فایل .db حاوی پشتیبان از پایگاه داده فعلی خود به دستگاهتان کلیک کنید." "importDatabase" = "بازیابی" "importDatabaseDesc" = "برای انتخاب و آپلود یک فایل .db از دستگاهتان و بازیابی پایگاه داده از یک پشتیبان کلیک کنید." "importDatabaseSuccess" = "پایگاه داده با موفقیت وارد شد" "importDatabaseError" = "خطا در وارد کردن پایگاه داده" "readDatabaseError" = "خطا در خواندن پایگاه داده" "getDatabaseError" = "خطا در دریافت پایگاه داده" "getConfigError" = "خطا در دریافت فایل پیکربندی" [pages.inbounds] "allTimeTraffic" = "کل ترافیک" "allTimeTrafficUsage" = "کل استفاده در تمام مدت" "title" = "کاربران" "totalDownUp" = "دریافت/ارسال کل" "totalUsage" = "‌‌‌مصرف کل" "inboundCount" = "کل ورودی‌ها" "operate" = "عملیات" "enable" = "فعال" "remark" = "نام" "protocol" = "پروتکل" "port" = "پورت" "portMap" = "پورت‌های نظیر" "traffic" = "ترافیک" "details" = "توضیحات" "transportConfig" = "نحوه اتصال" "expireDate" = "مدت زمان" "createdAt" = "ایجاد" "updatedAt" = "به‌روزرسانی" "resetTraffic" = "ریست ترافیک" "addInbound" = "افزودن ورودی" "generalActions" = "عملیات کلی" "autoRefresh" = "تازه‌سازی خودکار" "autoRefreshInterval" = "فاصله" "modifyInbound" = "ویرایش ورودی" "deleteInbound" = "حذف ورودی" "deleteInboundContent" = "آیا مطمئن به حذف ورودی هستید؟" "deleteClient" = "حذف کاربر" "deleteClientContent" = "آیا مطمئن به حذف کاربر هستید؟" "resetTrafficContent" = "آیا مطمئن به ریست ترافیک هستید؟" "copyLink" = "کپی لینک" "address" = "آدرس" "network" = "شبکه" "destinationPort" = "پورت مقصد" "targetAddress" = "آدرس مقصد" "monitorDesc" = "به‌طور پیش‌فرض خالی‌بگذارید" "meansNoLimit" = "0 = واحد: گیگابایت) نامحدود)" "totalFlow" = "ترافیک کل" "leaveBlankToNeverExpire" = "برای منقضی‌نشدن خالی‌بگذارید" "noRecommendKeepDefault" = "توصیه‌می‌شود به‌طور پیش‌فرض حفظ‌شود" "certificatePath" = "مسیر فایل" "certificateContent" = "محتوای فایل" "publicKey" = "کلید عمومی" "privatekey" = "کلید خصوصی" "clickOnQRcode" = "برای کپی بر روی کدتصویری کلیک کنید" "client" = "کاربر" "export" = "استخراج لینک‌ها" "clone" = "شبیه‌سازی" "cloneInbound" = "شبیه‌سازی ورودی" "cloneInboundContent" = "همه موارد این ورودی بجز پورت، آی‌پی و کاربر‌ها شبیه‌سازی خواهند شد" "cloneInboundOk" = "ساختن شبیه ساز" "resetAllTraffic" = "ریست ترافیک کل ورودی‌ها" "resetAllTrafficTitle" = "ریست ترافیک کل ورودی‌ها" "resetAllTrafficContent" = "آیا مطمئن به ریست ترافیک تمام ورودی‌ها هستید؟" "resetInboundClientTraffics" = "ریست ترافیک کاربران" "resetInboundClientTrafficTitle" = "ریست ترافیک کاربران" "resetInboundClientTrafficContent" = "آیا مطمئن به ریست ترافیک تمام کاربران این‌ ورودی هستید؟" "resetAllClientTraffics" = "ریست ترافیک کل کاربران" "resetAllClientTrafficTitle" = "ریست ترافیک کل کاربران" "resetAllClientTrafficContent" = "آیا مطمئن به ریست ترافیک تمام کاربران هستید؟" "delDepletedClients" = "حذف کاربران منقضی" "delDepletedClientsTitle" = "حذف کاربران منقضی" "delDepletedClientsContent" = "آیا مطمئن به حذف تمام کاربران منقضی‌شده ‌هستید؟" "email" = "ایمیل" "emailDesc" = "باید یک ایمیل یکتا باشد" "IPLimit" = "محدودیت آی‌پی" "IPLimitDesc" = "(اگر تعداد از مقدار تنظیم شده بیشتر شود، ورودی را غیرفعال می کند. (0 = غیرفعال" "IPLimitlog" = "گزارش‌ها" "IPLimitlogDesc" = "گزارش تاریخچه آی‌پی. برای فعال کردن ورودی پس از غیرفعال شدن، گزارش را پاک کنید" "IPLimitlogclear" = "پاک کردن گزارش‌ها" "setDefaultCert" = "استفاده از گواهی پنل" "telegramDesc" = "لطفا شناسه گفتگوی تلگرام را وارد کنید. (از دستور '/id' در ربات استفاده کنید) یا (@userinfobot)" "subscriptionDesc" = "شما می‌توانید لینک سابسکربپشن خودرا در 'جزئیات' پیدا کنید، همچنین می‌توانید از همین نام برای چندین کاربر استفاده‌کنید" "info" = "اطلاعات" "same" = "همسان" "inboundData" = "داده‌های ورودی" "exportInbound" = "استخراج ورودی" "import" = "افزودن" "importInbound" = "افزودن یک ورودی" "periodicTrafficResetTitle" = "بازنشانی ترافیک" "periodicTrafficResetDesc" = "بازنشانی خودکار شمارنده ترافیک در فواصل زمانی مشخص" "lastReset" = "آخرین بازنشانی" [pages.client] "add" = "کاربر جدید" "edit" = "ویرایش کاربر" "submitAdd" = "اضافه کردن" "submitEdit" = "ذخیره تغییرات" "clientCount" = "تعداد کاربران" "bulk" = "انبوه‌سازی" "method" = "روش" "first" = "از" "last" = "تا" "prefix" = "پیشوند" "postfix" = "پسوند" "delayedStart" = "شروع‌پس‌از‌اولین‌استفاده" "expireDays" = "مدت زمان" "days" = "(روز)" "renew" = "تمدید خودکار" "renewDesc" = "تمدید خودکار پس‌از ‌انقضا. (0 = غیرفعال)(واحد: روز)" [pages.inbounds.periodicTrafficReset] "never" = "هرگز" "daily" = "روزانه" "weekly" = "هفتگی" "monthly" = "ماهانه" [pages.inbounds.toasts] "obtain" = "فراهم‌سازی" "updateSuccess" = "بروزرسانی با موفقیت انجام شد" "logCleanSuccess" = "لاگ پاکسازی شد" "inboundsUpdateSuccess" = "ورودی‌ها با موفقیت به‌روزرسانی شدند" "inboundUpdateSuccess" = "ورودی با موفقیت به‌روزرسانی شد" "inboundCreateSuccess" = "ورودی با موفقیت ایجاد شد" "inboundDeleteSuccess" = "ورودی با موفقیت حذف شد" "inboundClientAddSuccess" = "کلاینت(های) ورودی اضافه شدند" "inboundClientDeleteSuccess" = "کلاینت ورودی حذف شد" "inboundClientUpdateSuccess" = "کلاینت ورودی به‌روزرسانی شد" "delDepletedClientsSuccess" = "تمام کلاینت‌های مصرف شده حذف شدند" "resetAllClientTrafficSuccess" = "تمام ترافیک کلاینت بازنشانی شد" "resetAllTrafficSuccess" = "تمام ترافیک‌ها بازنشانی شدند" "resetInboundClientTrafficSuccess" = "ترافیک بازنشانی شد" "trafficGetError" = "خطا در دریافت ترافیک‌ها" "getNewX25519CertError" = "خطا در دریافت گواهی X25519." "getNewmldsa65Error" = "خطا در دریافت گواهی mldsa65." "getNewVlessEncError" = "خطا در دریافت گواهی VlessEnc." [pages.inbounds.stream.general] "request" = "درخواست" "response" = "پاسخ" "name" = "نام" "value" = "مقدار" [pages.inbounds.stream.tcp] "version" = "نسخه" "method" = "متد" "path" = "مسیر" "status" = "وضعیت" "statusDescription" = "توضیحات وضعیت" "requestHeader" = "سربرگ درخواست" "responseHeader" = "سربرگ پاسخ" [pages.settings] "title" = "تنظیمات پنل" "save" = "ذخیره" "infoDesc" = "برای اعمال تغییرات در این بخش باید پس از ذخیره کردن، پنل را ریستارت کنید" "restartPanel" = "ریستارت پنل" "restartPanelDesc" = "آیا مطمئن به ریستارت پنل هستید؟ اگر پس‌از ریستارت نمی‌توانید به پنل دسترسی پیدا کنید، لطفاً گزارش‌های موجود در اسکریپت پنل را بررسی کنید" "restartPanelSuccess" = "پنل با موفقیت راه‌اندازی مجدد شد" "actions" = "عملیات ها" "resetDefaultConfig" = "برگشت به پیش‌فرض" "panelSettings" = "پیکربندی" "securitySettings" = "احرازهویت" "TGBotSettings" = "ربات تلگرام" "panelListeningIP" = "آدرس آی‌پی" "panelListeningIPDesc" = "آدرس آی‌پی برای وب پنل. برای گوش‌دادن به‌تمام آی‌پی‌ها خالی‌بگذارید" "panelListeningDomain" = "نام دامنه" "panelListeningDomainDesc" = "آدرس دامنه برای وب پنل. برای گوش دادن به‌تمام دامنه‌ها و آی‌پی‌ها خالی‌بگذارید" "panelPort" = "پورت" "panelPortDesc" = "شماره پورت برای وب پنل. باید پورت استفاده نشده‌باشد" "publicKeyPath" = "مسیر کلید عمومی" "publicKeyPathDesc" = "مسیر فایل کلیدعمومی برای وب پنل. با '/' شروع‌می‌شود" "privateKeyPath" = "مسیر کلید خصوصی" "privateKeyPathDesc" = "مسیر فایل کلیدخصوصی برای وب پنل. با '/' شروع‌می‌شود" "panelUrlPath" = "URI مسیر" "panelUrlPathDesc" = "برای وب پنل. با '/' شروع‌ و با '/' خاتمه‌ می‌یابد URI مسیر" "pageSize" = "اندازه صفحه بندی جدول" "pageSizeDesc" = "(اندازه صفحه برای جدول ورودی‌ها.(0 = غیرفعال" "remarkModel" = "نام‌کانفیگ و جداکننده" "datepicker" = "نوع تقویم" "datepickerPlaceholder" = "انتخاب تاریخ" "datepickerDescription" = "وظایف برنامه ریزی شده بر اساس این تقویم اجرا می‌شود" "sampleRemark" = "نمونه‌نام" "oldUsername" = "نام‌کاربری فعلی" "currentPassword" = "رمز‌عبور فعلی" "newUsername" = "نام‌کاربری جدید" "newPassword" = "رمزعبور جدید" "telegramBotEnable" = "فعال‌سازی ربات تلگرام" "telegramBotEnableDesc" = "ربات تلگرام را فعال می‌کند" "telegramToken" = "توکن تلگرام" "telegramTokenDesc" = "دریافت کنید @botfather توکن را می‌توانید از" "telegramProxy" = "SOCKS پراکسی" "telegramProxyDesc" = "را برای اتصال به تلگرام فعال می کند SOCKS5 پراکسی" "telegramAPIServer" = "سرور API تلگرام" "telegramAPIServerDesc" = "API سرور تلگرام برای اتصال را تغییر میدهد. برای استفاده از سرور پیش فرض خالی بگذارید" "telegramChatId" = "آی‌دی چت مدیر" "telegramChatIdDesc" = "دریافت ‌کنید ('/id'یا (دستور (@userinfobot) آی‌دی(های) چت تلگرام مدیر، از" "telegramNotifyTime" = "زمان نوتیفیکیشن" "telegramNotifyTimeDesc" = "زمان‌اطلاع‌رسانی ربات تلگرام برای گزارش های دوره‌ای. از فرمت زمانبندی لینوکس استفاده‌کنید‌" "tgNotifyBackup" = "پشتیبان‌گیری از دیتابیس" "tgNotifyBackupDesc" = "فایل پشتیبان‌دیتابیس را به‌همراه گزارش ارسال می‌کند" "tgNotifyLogin" = "اعلان ورود" "tgNotifyLoginDesc" = "نام‌کاربری، آدرس آی‌پی، و زمان ورود، فردی که سعی می‌کند وارد پنل شود را نمایش می‌دهد" "sessionMaxAge" = "بیشینه زمان جلسه وب" "sessionMaxAgeDesc" = "(بیشینه زمانی که می‌توانید لاگین بمانید. (واحد: دقیقه" "expireTimeDiff" = "آستانه زمان باقی مانده" "expireTimeDiffDesc" = "(فاصله زمانی هشدار تا رسیدن به زمان انقضا. (واحد: روز" "trafficDiff" = "آستانه ترافیک باقی مانده" "trafficDiffDesc" = "(فاصله زمانی هشدار تا رسیدن به اتمام ترافیک. (واحد: گیگابایت" "tgNotifyCpu" = "آستانه هشدار بار پردازنده" "tgNotifyCpuDesc" = "(اگر بار روی پردازنده ازاین آستانه فراتر رفت، برای شما پیام ارسال می‌شود. (واحد: درصد" "timeZone" = "منطقه زمانی" "timeZoneDesc" = "وظایف برنامه ریزی شده بر اساس این منطقه‌زمانی اجرا می‌شود" "subSettings" = "سابسکریپشن" "subEnable" = "فعال‌سازی سرویس سابسکریپشن" "subEnableDesc" = "سرویس سابسکریپشن‌ را فعال‌می‌کند" "subJsonEnable" = "فعال/غیرفعال‌سازی مستقل نقطه دسترسی سابسکریپشن JSON." "subTitle" = "عنوان اشتراک" "subTitleDesc" = "عنوان نمایش داده شده در کلاینت VPN" "subSupportUrl" = "آدرس پشتیبانی" "subSupportUrlDesc" = "لینک پشتیبانی فنی که در کلاینت VPN نمایش داده می‌شود" "subProfileUrl" = "آدرس پروفایل" "subProfileUrlDesc" = "لینک وب‌سایت شما که در کلاینت VPN نمایش داده می‌شود" "subAnnounce" = "اعلان" "subAnnounceDesc" = "متن اعلانی که در کلاینت VPN نمایش داده می‌شود" "subEnableRouting" = "فعال‌سازی مسیریابی" "subEnableRoutingDesc" = "تنظیمات سراسری برای فعال‌سازی مسیریابی در کلاینت VPN. (فقط برای Happ)" "subRoutingRules" = "قوانین مسیریابی" "subRoutingRulesDesc" = "قوانین مسیریابی سراسری برای کلاینت VPN. (فقط برای Happ)" "subListen" = "آدرس آی‌پی" "subListenDesc" = "آدرس آی‌پی برای سرویس سابسکریپشن. برای گوش دادن به‌تمام آی‌پی‌ها خالی‌بگذارید" "subPort" = "پورت" "subPortDesc" = "شماره پورت برای سرویس سابسکریپشن. باید پورت استفاده نشده‌باشد" "subCertPath" = "مسیر کلید عمومی" "subCertPathDesc" = "مسیر فایل کلیدعمومی برای سرویس سابیکریپشن. با '/' شروع‌می‌شود" "subKeyPath" = "مسیر کلید خصوصی" "subKeyPathDesc" = "مسیر فایل کلیدخصوصی برای سرویس سابسکریپشن. با '/' شروع‌می‌شود" "subPath" = "URI مسیر" "subPathDesc" = "برای سرویس سابسکریپشن. با '/' شروع‌ و با '/' خاتمه‌ می‌یابد URI مسیر" "subDomain" = "نام دامنه" "subDomainDesc" = "آدرس دامنه برای سرویس سابسکریپشن. برای گوش دادن به تمام دامنه‌ها و آی‌پی‌ها خالی‌بگذارید‌" "subUpdates" = "فاصله بروزرسانی‌ سابسکریپشن" "subUpdatesDesc" = "(فاصله مابین بروزرسانی در برنامه‌های کاربری. (واحد: ساعت" "subEncrypt" = "کدگذاری" "subEncryptDesc" = "کدگذاری خواهدشد Base64 محتوای برگشتی سرویس سابسکریپشن برپایه" "subShowInfo" = "نمایش اطلاعات مصرف" "subShowInfoDesc" = "ترافیک و زمان باقی‌مانده را در برنامه‌های کاربری نمایش می‌دهد" "subURI" = "پروکسی معکوس URI مسیر" "subURIDesc" = "سابسکریپشن را برای استفاده در پشت پراکسی‌ها تغییر می‌دهد URI مسیر" "externalTrafficInformEnable" = "اطلاع رسانی خارجی مصرف ترافیک" "externalTrafficInformEnableDesc" = "مصرف ترافیک به سرویس خارجی ارسال می شود" "externalTrafficInformURI" = "لینک اطلاع رسانی خارجی مصرف ترافیک" "externalTrafficInformURIDesc" = "ترافیک های مصرفی به این لینک هم ارسال می شود" "fragment" = "فرگمنت" "fragmentDesc" = "فعال کردن فرگمنت برای بسته‌ی نخست تی‌ال‌اس" "fragmentSett" = "تنظیمات فرگمنت" "noisesDesc" = "فعال کردن Noises." "noisesSett" = "تنظیمات Noises" "mux" = "ماکس" "muxDesc" = "چندین جریان داده مستقل را در یک جریان داده ثابت منتقل می کند" "muxSett" = "تنظیمات ماکس" "direct" = "اتصال مستقیم" "directDesc" = "به طور مستقیم با دامنه ها یا محدوده آی‌پی یک کشور خاص ارتباط برقرار می کند" "notifications" = "اعلان‌ها" "certs" = "گواهی‌ها" "externalTraffic" = "ترافیک خارجی" "dateAndTime" = "تاریخ و زمان" "proxyAndServer" = "پراکسی و سرور" "intervals" = "فواصل" "information" = "اطلاعات" "language" = "زبان" "telegramBotLanguage" = "زبان ربات تلگرام" [pages.xray] "title" = "پیکربندی ایکس‌ری" "save" = "ذخیره" "restart" = "ریستارت ایکس‌ری" "restartSuccess" = "Xray با موفقیت راه‌اندازی مجدد شد" "stopSuccess" = "Xray با موفقیت متوقف شد" "restartError" = "خطا در راه‌اندازی مجدد Xray." "stopError" = "خطا در توقف Xray." "basicTemplate" = "پایه" "advancedTemplate" = "پیشرفته" "generalConfigs" = "استراتژی‌ کلی" "generalConfigsDesc" = "این گزینه‌ها استراتژی کلی ترافیک را تعیین می‌کنند" "logConfigs" = "گزارش" "logConfigsDesc" = "گزارش‌ها ممکن است بر کارایی سرور شما تأثیر بگذارد. توصیه می شود فقط در صورت نیاز آن را عاقلانه فعال کنید" "blockConfigsDesc" = "این گزینه‌ها ترافیک را بر اساس پروتکل‌های درخواستی خاص، و وب سایت‌ها مسدود می‌کند" "basicRouting" = "مسیریابی پایه" "blockConnectionsConfigsDesc" = "این گزینه‌ها ترافیک را بر اساس کشور درخواست‌شده خاص مسدود می‌کنند." "directConnectionsConfigsDesc" = "یک اتصال مستقیم تضمین می‌کند که ترافیک خاص از طریق سرور دیگری مسیریابی نشود." "blockips" = "مسدود کردن آی‌پی‌ها" "blockdomains" = "مسدود کردن دامنه‌ها" "directips" = "آی‌پی‌های مستقیم" "directdomains" = "دامنه‌های مستقیم" "ipv4Routing" = "IPv4 مسیریابی" "ipv4RoutingDesc" = "این گزینه‌ها ترافیک را از طریق آی‌پی نسخه4 سرور، به مقصد هدایت می‌کند" "warpRouting" = "WARP مسیریابی" "warpRoutingDesc" = "این گزینه‌ها ترافیک‌ را از طریق وارپ کلادفلر به مقصد هدایت می‌کند" "Template" = "‌پیکربندی پیشرفته الگو ایکس‌ری" "TemplateDesc" = "فایل پیکربندی نهایی ایکس‌ری بر اساس این الگو ایجاد می‌شود" "FreedomStrategy" = "Freedom استراتژی پروتکل" "FreedomStrategyDesc" = "تعیین می‌کند Freedom استراتژی خروجی شبکه را برای پروتکل" "RoutingStrategy" = "استراتژی کلی مسیریابی" "RoutingStrategyDesc" = "استراتژی کلی مسیریابی برای حل تمام درخواست‌ها را تعیین می‌کند" "outboundTestUrl" = "آدرس تست خروجی" "outboundTestUrlDesc" = "آدرسی که برای تست اتصال خروجی استفاده می‌شود." "Torrent" = "مسدودسازی پروتکل بیت‌تورنت" "Inbounds" = "ورودی‌ها" "InboundsDesc" = "پذیرش کلاینت خاص" "Outbounds" = "خروجی‌ها" "Balancers" = "بالانسرها" "OutboundsDesc" = "مسیر ترافیک خروجی را تنظیم کنید" "Routings" = "قوانین مسیریابی" "RoutingsDesc" = "اولویت هر قانون مهم است" "completeTemplate" = "کامل" "logLevel" = "سطح گزارش" "logLevelDesc" = "سطح گزارش برای گزارش های خطا، نشان دهنده اطلاعاتی است که باید ثبت شوند." "accessLog" = "مسیر گزارش" "accessLogDesc" = "مسیر فایل برای گزارش دسترسی. مقدار ویژه «هیچ» گزارش‌های دسترسی را غیرفعال میکند." "errorLog" = "گزارش خطا" "errorLogDesc" = "مسیر فایل برای ورود به سیستم خطا. مقدار ویژه «هیچ» گزارش های خطا را غیرفعال میکند" "dnsLog" = "گزارش DNS" "dnsLogDesc" = "آیا ثبت‌های درخواست DNS را فعال کنید" "maskAddress" = "پنهان کردن آدرس" "maskAddressDesc" = "پوشش آدرس IP، هنگامی که فعال می‌شود، به طور خودکار آدرس IP که در لاگ ظاهر می‌شود را جایگزین می‌کند." "statistics" = "آمار" "statsInboundUplink" = "آمار آپلود ورودی" "statsInboundUplinkDesc" = "جمع‌آوری آمار برای ترافیک بالارو (آپلود) تمام پروکسی‌های ورودی را فعال می‌کند." "statsInboundDownlink" = "آمار دانلود ورودی" "statsInboundDownlinkDesc" = "جمع‌آوری آمار برای ترافیک پایین‌رو (دانلود) تمام پروکسی‌های ورودی را فعال می‌کند." "statsOutboundUplink" = "آمار آپلود خروجی" "statsOutboundUplinkDesc" = "جمع‌آوری آمار برای ترافیک بالارو (آپلود) تمام پروکسی‌های خروجی را فعال می‌کند." "statsOutboundDownlink" = "آمار دانلود خروجی" "statsOutboundDownlinkDesc" = "جمع‌آوری آمار برای ترافیک پایین‌رو (دانلود) تمام پروکسی‌های خروجی را فعال می‌کند." [pages.xray.rules] "first" = "اولین" "last" = "آخرین" "up" = "بالا" "down" = "پایین" "source" = "مبدا" "dest" = "مقصد" "inbound" = "ورودی" "outbound" = "خروجی" "balancer" = "بالانسر" "info" = "اطلاعات" "add" = "افزودن قانون" "edit" = "ویرایش قانون" "useComma" = "موارد جدا شده با کاما" [pages.xray.outbound] "addOutbound" = "افزودن خروجی" "addReverse" = "افزودن معکوس" "editOutbound" = "ویرایش خروجی" "editReverse" = "ویرایش معکوس" "tag" = "برچسب" "tagDesc" = "برچسب یگانه" "address" = "آدرس" "reverse" = "معکوس" "domain" = "دامنه" "type" = "نوع" "bridge" = "پل" "portal" = "پورتال" "link" = "لینک" "intercon" = "اتصال میانی" "settings" = "تنظیمات" "accountInfo" = "اطلاعات حساب" "outboundStatus" = "وضعیت خروجی" "sendThrough" = "ارسال با" "test" = "تست" "testResult" = "نتیجه تست" "testing" = "در حال تست اتصال..." "testSuccess" = "تست موفقیت‌آمیز" "testFailed" = "تست ناموفق" "testError" = "خطا در تست خروجی" [pages.xray.balancer] "addBalancer" = "افزودن بالانسر" "editBalancer" = "ویرایش بالانسر" "balancerStrategy" = "استراتژی" "balancerSelectors" = "انتخاب‌گرها" "tag" = "برچسب" "tagDesc" = "برچسب یگانه" "balancerDesc" = "امکان استفاده همزمان balancerTag و outboundTag باهم وجود ندارد. درصورت استفاده همزمان فقط outboundTag عمل خواهد کرد." [pages.xray.wireguard] "secretKey" = "کلید شخصی" "publicKey" = "کلید عمومی" "allowedIPs" = "آی‌پی‌های مجاز" "endpoint" = "نقطه پایانی" "psk" = "کلید مشترک" "domainStrategy" = "استراتژی حل دامنه" [pages.xray.tun] "nameDesc" = "نام رابط TUN. مقدار پیش‌فرض 'xray0' است" "mtuDesc" = "واحد انتقال حداکثر. بیشترین اندازه بسته‌های داده. مقدار پیش‌فرض 1500 است" "userLevel" = "سطح کاربر" "userLevelDesc" = "تمام اتصالات انجام‌شده از طریق این ورودی از این سطح کاربری استفاده خواهند کرد. مقدار پیش‌فرض 0 است" [pages.xray.dns] "enable" = "فعال کردن حل دامنه" "enableDesc" = "سرور حل دامنه داخلی را فعال کنید" "tag" = "برچسب" "tagDesc" = "این برچسب در قوانین مسیریابی به عنوان یک برچسب ورودی قابل استفاده خواهد بود" "clientIp" = "آی‌پی کلاینت" "clientIpDesc" = "برای اطلاع‌رسانی به سرور درباره مکان IP مشخص‌شده در طول درخواست‌های DNS استفاده می‌شود" "disableCache" = "غیرفعال‌سازی کش" "disableCacheDesc" = "کش DNS را غیرفعال می‌کند" "disableFallback" = "غیرفعال‌سازی Fallback" "disableFallbackDesc" = "درخواست‌های DNS Fallback را غیرفعال می‌کند" "disableFallbackIfMatch" = "غیرفعال‌سازی Fallback در صورت تطابق" "disableFallbackIfMatchDesc" = "درخواست‌های DNS Fallback را زمانی که لیست دامنه‌های مطابقت‌یافته سرور DNS فعال است، غیرفعال می‌کند" "enableParallelQuery" = "فعال‌سازی پرس‌وجوی موازی" "enableParallelQueryDesc" = "فعال‌سازی پرس‌وجوهای DNS موازی به چندین سرور برای وضوح سریع‌تر" "strategy" = "استراتژی پرس‌وجو" "strategyDesc" = "استراتژی کلی برای حل نام دامنه" "add" = "افزودن سرور" "edit" = "ویرایش سرور" "domains" = "دامنه‌ها" "expectIPs" = "آی‌پی‌های مورد انتظار" "unexpectIPs" = "آی‌پی‌های غیرمنتظره" "useSystemHosts" = "استفاده از Hosts سیستم" "useSystemHostsDesc" = "استفاده از فایل hosts یک سیستم نصب‌شده" "usePreset" = "استفاده از پیش‌تنظیم" "dnsPresetTitle" = "پیش‌تنظیم‌های DNS" "dnsPresetFamily" = "خانوادگی" [pages.xray.fakedns] "add" = "افزودن دی‌ان‌اس جعلی" "edit" = "ویرایش دی‌ان‌اس جعلی" "ipPool" = "زیرشبکه استخر آی‌پی" "poolSize" = "اندازه استخر" [pages.settings.security] "admin" = "اعتبارنامه‌های ادمین" "twoFactor" = "احراز هویت دو مرحله‌ای" "twoFactorEnable" = "فعال‌سازی 2FA" "twoFactorEnableDesc" = "یک لایه اضافی امنیتی برای احراز هویت فراهم می‌کند." "twoFactorModalSetTitle" = "فعال‌سازی احراز هویت دو مرحله‌ای" "twoFactorModalDeleteTitle" = "غیرفعال‌سازی احراز هویت دو مرحله‌ای" "twoFactorModalSteps" = "برای راه‌اندازی احراز هویت دو مرحله‌ای، مراحل زیر را انجام دهید:" "twoFactorModalFirstStep" = "1. این کد QR را در برنامه احراز هویت اسکن کنید یا توکن کنار کد QR را کپی کرده و در برنامه بچسبانید" "twoFactorModalSecondStep" = "2. کد را از برنامه وارد کنید" "twoFactorModalRemoveStep" = "برای حذف احراز هویت دو مرحله‌ای، کد را از برنامه وارد کنید." "twoFactorModalChangeCredentialsTitle" = "تغییر اعتبارنامه‌ها" "twoFactorModalChangeCredentialsStep" = "برای تغییر اعتبارنامه‌های مدیر، کد را از برنامه وارد کنید." "twoFactorModalSetSuccess" = "احراز هویت دو مرحله‌ای با موفقیت برقرار شد" "twoFactorModalDeleteSuccess" = "احراز هویت دو مرحله‌ای با موفقیت حذف شد" "twoFactorModalError" = "کد نادرست" [pages.settings.toasts] "modifySettings" = "پارامترها تغییر کرده‌اند." "getSettings" = "خطا در دریافت پارامترها" "modifyUserError" = "خطا در تغییر اعتبارنامه‌های مدیر سیستم." "modifyUser" = "شما با موفقیت اعتبارنامه‌های مدیر سیستم را تغییر دادید." "originalUserPassIncorrect" = "نام‌کاربری یا رمزعبور فعلی اشتباه‌است" "userPassMustBeNotEmpty" = "نام‌کاربری یا رمزعبور جدید خالی‌است" "getOutboundTrafficError" = "خطا در دریافت ترافیک خروجی" "resetOutboundTrafficError" = "خطا در بازنشانی ترافیک خروجی" [tgbot] "keyboardClosed" = "❌ صفحه کلید بسته شد!" "noResult" = "❗ نتیجه ای یافت نشد!" "noQuery" = "❌ درخواست یافت نشد! لطفا دوباره تلاش کنید!" "wentWrong" = "❌ مشکلی پیش آمد!" "noIpRecord" = "❗ رکورد آی پی وجود ندارد!" "noInbounds" = "❗ هیچ ورودی یافت نشد!" "unlimited" = "♾ نامحدود(ریست)" "add" = "افزودن" "month" = "ماه" "months" = "ماه" "day" = "روز" "days" = "روز" "hours" = "ساعت" "minutes" = "دقیقه" "unknown" = "نامشخص" "inbounds" = "ورودی ها" "clients" = "کاربران" "offline" = "🔴 آفلاین" "online" = "🟢 آنلاین" [tgbot.commands] "unknown" = "❗ دستور ناشناخته" "pleaseChoose" = "👇 لطفاً انتخاب کنید:\r\n" "help" = "🤖 به این ربات خوش آمدید! این ربات برای ارائه داده‌های خاص از سرور طراحی شده است و به شما امکان تغییرات لازم را می‌دهد.\r\n\r\n" "start" = "👋 سلام {{ .Firstname }}.\r\n" "welcome" = "🤖 به ربات مدیریت {{ .Hostname }} خوش آمدید.\r\n" "status" = "✅ ربات در حالت عادی است!" "usage" = "❗ لطفاً یک متن برای جستجو وارد کنید!" "getID" = "🆔 شناسه شما: {{ .ID }}" "helpAdminCommands" = "برای راه‌اندازی مجدد Xray Core:\r\n/restart\r\n\r\nبرای جستجوی ایمیل مشتری:\r\n/usage [ایمیل]\r\n\r\nبرای جستجوی ورودی‌ها (با آمار مشتری):\r\n/inbound [توضیحات]\r\n\r\nشناسه گفتگوی تلگرام:\r\n/id" "helpClientCommands" = "برای جستجوی آمار، از دستور زیر استفاده کنید:\r\n/usage [ایمیل]\r\n\r\nشناسه گفتگوی تلگرام:\r\n/id" "restartUsage" = "\r\n\r\n/restart" "restartSuccess" = "✅ عملیات با موفقیت انجام شد!" "restartFailed" = "❗ خطا در عملیات.\r\n\r\nخطا: {{ .Error }}." "xrayNotRunning" = "❗ Xray Core در حال اجرا نیست." "startDesc" = "نمایش منوی اصلی" "helpDesc" = "راهنمای ربات" "statusDesc" = "بررسی وضعیت ربات" "idDesc" = "نمایش شناسه تلگرام شما" [tgbot.messages] "cpuThreshold" = "🔴 بار ‌پردازنده {{ .Percent }}% بیشتر از آستانه است {{ .Threshold }}%" "selectUserFailed" = "❌ خطا در انتخاب کاربر!" "userSaved" = "✅ کاربر تلگرام ذخیره شد." "loginSuccess" = "✅ با موفقیت به پنل وارد شدید.\r\n" "loginFailed" = "❗️ ورود به پنل ناموفق‌بود \r\n" "2faFailed" = "خطای 2FA" "report" = "🕰 گزارشات‌زمان‌بندی‌شده: {{ .RunTime }}\r\n" "datetime" = "⏰ تاریخ‌وزمان: {{ .DateTime }}\r\n" "hostname" = "💻 نام‌میزبان: {{ .Hostname }}\r\n" "version" = "🚀 نسخه‌پنل: {{ .Version }}\r\n" "xrayVersion" = "📡 نسخه‌هسته: {{ .XrayVersion }}\r\n" "ipv6" = "🌐 IPv6: {{ .IPv6 }}\r\n" "ipv4" = "🌐 IPv4: {{ .IPv4 }}\r\n" "ip" = "🌐 آدرس‌آی‌پی: {{ .IP }}\r\n" "ips" = "🔢 آدرس‌های آی‌پی:\r\n{{ .IPs }}\r\n" "serverUpTime" = "⏳ مدت‌کارکردسیستم: {{ .UpTime }} {{ .Unit }}\r\n" "serverLoad" = "📈 بارسیستم: {{ .Load1 }}, {{ .Load2 }}, {{ .Load3 }}\r\n" "serverMemory" = "📋 RAM: {{ .Current }}/{{ .Total }}\r\n" "tcpCount" = "🔹 TCP: {{ .Count }}\r\n" "udpCount" = "🔸 UDP: {{ .Count }}\r\n" "traffic" = "🚦 ترافیک: {{ .Total }} (↑{{ .Upload }},↓{{ .Download }})\r\n" "xrayStatus" = "ℹ️ وضعیت‌ایکس‌ری: {{ .State }}\r\n" "username" = "👤 نام‌کاربری: {{ .Username }}\r\n" "password" = "👤 رمز عبور: {{ .Password }}\r\n" "time" = "⏰ زمان: {{ .Time }}\r\n" "inbound" = "📍 نام‌ورودی: {{ .Remark }}\r\n" "port" = "🔌 پورت: {{ .Port }}\r\n" "expire" = "📅 تاریخ‌انقضا: {{ .Time }}\r\n\r\n" "expireIn" = "📅 باقی‌ مانده‌ تا انقضا: {{ .Time }}\r\n\r\n" "active" = "💡 فعال: {{ .Enable }}\r\n" "enabled" = "🚨 وضعیت: {{ .Enable }}\r\n" "online" = "🌐 وضعیت اتصال: {{ .Status }}\r\n" "lastOnline" = "🔙 آخرین فعالیت: {{ .Time }}\r\n" "email" = "📧 ایمیل: {{ .Email }}\r\n" "upload" = "🔼 آپلود↑: {{ .Upload }}\r\n" "download" = "🔽 دانلود↓: {{ .Download }}\r\n" "total" = "🔄 کل: {{ .UpDown }} / {{ .Total }}\r\n" "TGUser" = "👤 کاربر تلگرام: {{ .TelegramID }}\r\n" "exhaustedMsg" = "🚨 {{ .Type }} به‌اتمام‌رسیده‌است:\r\n" "exhaustedCount" = "🚨 تعداد {{ .Type }} به‌اتمام‌رسیده‌است:\r\n" "onlinesCount" = "🌐 کاربران‌آنلاین: {{ .Count }}\r\n" "disabled" = "🛑 غیرفعال: {{ .Disabled }}\r\n" "depleteSoon" = "🔜 به‌زودی‌به‌پایان‌خواهدرسید: {{ .Deplete }}\r\n\r\n" "backupTime" = "🗄 زمان‌پشتیبان‌گیری: {{ .Time }}\r\n" "refreshedOn" = "\r\n📋🔄 تازه‌سازی شده در: {{ .Time }}\r\n\r\n" "yes" = "✅ بله" "no" = "❌ خیر" "received_id" = "🔑📥 شناسه به‌روزرسانی شد." "received_password" = "🔑📥 رمز عبور به‌روزرسانی شد." "received_email" = "📧📥 ایمیل به‌روزرسانی شد." "received_comment" = "💬📥 نظر به‌روزرسانی شد." "id_prompt" = "🔑 شناسه پیش‌فرض: {{ .ClientId }}\n\nشناسه خود را وارد کنید." "pass_prompt" = "🔑 رمز عبور پیش‌فرض: {{ .ClientPassword }}\n\nرمز عبور خود را وارد کنید." "email_prompt" = "📧 ایمیل پیش‌فرض: {{ .ClientEmail }}\n\nایمیل خود را وارد کنید." "comment_prompt" = "💬 نظر پیش‌فرض: {{ .ClientComment }}\n\nنظر خود را وارد کنید." "inbound_client_data_id" = "🔄 ورودی: {{ .InboundRemark }}\n\n🔑 شناسه: {{ .ClientId }}\n📧 ایمیل: {{ .ClientEmail }}\n📊 ترافیک: {{ .ClientTraffic }}\n📅 تاریخ انقضا: {{ .ClientExp }}\n🌐 محدودیت IP: {{ .IpLimit }}\n💬 توضیح: {{ .ClientComment }}\n\nاکنون می‌تونی مشتری را به ورودی اضافه کنی!" "inbound_client_data_pass" = "🔄 ورودی: {{ .InboundRemark }}\n\n🔑 رمز عبور: {{ .ClientPass }}\n📧 ایمیل: {{ .ClientEmail }}\n📊 ترافیک: {{ .ClientTraffic }}\n📅 تاریخ انقضا: {{ .ClientExp }}\n🌐 محدودیت IP: {{ .IpLimit }}\n💬 توضیح: {{ .ClientComment }}\n\nاکنون می‌تونی مشتری را به ورودی اضافه کنی!" "cancel" = "❌ فرآیند لغو شد! \n\nمی‌توانید هر زمان که خواستید /start را دوباره اجرا کنید. 🔄" "error_add_client" = "⚠️ خطا:\n\n {{ .error }}" "using_default_value" = "باشه، از مقدار پیش‌فرض استفاده می‌کنم. 😊" "incorrect_input" = "ورودی شما معتبر نیست.\nعبارت‌ها باید بدون فاصله باشند.\nمثال صحیح: aaaaaa\nمثال نادرست: aaa aaa 🚫" "AreYouSure" = "مطمئنی؟ 🤔" "SuccessResetTraffic" = "📧 ایمیل: {{ .ClientEmail }}\n🏁 نتیجه: ✅ موفقیت‌آمیز" "FailedResetTraffic" = "📧 ایمیل: {{ .ClientEmail }}\n🏁 نتیجه: ❌ ناموفق \n\n🛠️ خطا: [ {{ .ErrorMessage }} ]" "FinishProcess" = "🔚 فرآیند بازنشانی ترافیک برای همه مشتریان به پایان رسید." [tgbot.buttons] "closeKeyboard" = "❌ بستن کیبورد" "cancel" = "❌ لغو" "cancelReset" = "❌ لغو تنظیم مجدد" "cancelIpLimit" = "❌ لغو محدودیت آی‌پی" "confirmResetTraffic" = "✅ تأیید تنظیم مجدد ترافیک؟" "confirmClearIps" = "✅ تأیید پاک‌سازی آدرس‌های آی‌پی؟" "confirmRemoveTGUser" = "✅ تأیید حذف کاربر تلگرام؟" "confirmToggle" = "✅ تایید فعال/غیرفعال کردن کاربر؟" "dbBackup" = "دریافت پشتیبان" "serverUsage" = "استفاده از سیستم" "getInbounds" = "دریافت ورودی‌ها" "depleteSoon" = "به‌زودی به پایان خواهد رسید" "clientUsage" = "دریافت آمار کاربر" "onlines" = "کاربران آنلاین" "commands" = "دستورات" "refresh" = "🔄 تازه‌سازی" "clearIPs" = "❌ پاک‌سازی آدرس‌ها" "removeTGUser" = "❌ حذف کاربر تلگرام" "selectTGUser" = "👤 انتخاب کاربر تلگرام" "selectOneTGUser" = "👤 یک کاربر تلگرام را انتخاب کنید:" "resetTraffic" = "📈 تنظیم مجدد ترافیک" "resetExpire" = "📅 تنظیم مجدد تاریخ انقضا" "ipLog" = "🔢 لاگ آدرس‌های IP" "ipLimit" = "🔢 محدودیت IP" "setTGUser" = "👤 تنظیم کاربر تلگرام" "toggle" = "🔘 فعال / غیرفعال" "custom" = "🔢 سفارشی" "confirmNumber" = "✅ تایید: {{ .Num }}" "confirmNumberAdd" = "✅ تایید اضافه کردن: {{ .Num }}" "limitTraffic" = "🚧 محدودیت ترافیک" "getBanLogs" = "گزارش های بلوک را دریافت کنید" "allClients" = "همه مشتریان" "addClient" = "افزودن مشتری" "submitDisable" = "ارسال به عنوان غیرفعال ☑️" "submitEnable" = "ارسال به عنوان فعال ✅" "use_default" = "🏷️ استفاده از پیش‌فرض" "change_id" = "⚙️🔑 شناسه" "change_password" = "⚙️🔑 گذرواژه" "change_email" = "⚙️📧 ایمیل" "change_comment" = "⚙️💬 نظر" "ResetAllTraffics" = "بازنشانی همه ترافیک‌ها" "SortedTrafficUsageReport" = "گزارش استفاده از ترافیک مرتب‌شده" [tgbot.answers] "successfulOperation" = "✅ انجام شد!" "errorOperation" = "❗ خطا در عملیات." "getInboundsFailed" = "❌ دریافت ورودی‌ها با خطا مواجه شد." "getClientsFailed" = "❌ دریافت مشتریان با شکست مواجه شد." "canceled" = "❌ {{ .Email }} : عملیات لغو شد." "clientRefreshSuccess" = "✅ {{ .Email }} : کلاینت با موفقیت تازه‌سازی شد." "IpRefreshSuccess" = "✅ {{ .Email }} : آدرس‌ها با موفقیت تازه‌سازی شدند." "TGIdRefreshSuccess" = "✅ {{ .Email }} : کاربر تلگرام کلاینت با موفقیت تازه‌سازی شد." "resetTrafficSuccess" = "✅ {{ .Email }} : ترافیک با موفقیت تنظیم مجدد شد." "setTrafficLimitSuccess" = "✅ {{ .Email }} : محدودیت ترافیک با موفقیت ذخیره شد." "expireResetSuccess" = "✅ {{ .Email }} : تاریخ انقضا با موفقیت تنظیم مجدد شد." "resetIpSuccess" = "✅ {{ .Email }} : محدودیت آدرس IP {{ .Count }} با موفقیت ذخیره شد." "clearIpSuccess" = "✅ {{ .Email }} : آدرس‌ها با موفقیت پاک‌سازی شدند." "getIpLog" = "✅ {{ .Email }} : دریافت لاگ آدرس‌های IP." "getUserInfo" = "✅ {{ .Email }} : دریافت اطلاعات کاربر تلگرام." "removedTGUserSuccess" = "✅ {{ .Email }} : کاربر تلگرام با موفقیت حذف شد." "enableSuccess" = "✅ {{ .Email }} : با موفقیت فعال شد." "disableSuccess" = "✅ {{ .Email }} : با موفقیت غیرفعال شد." "askToAddUserId" = "پیکربندی شما یافت نشد!\r\nلطفاً از مدیر خود بخواهید که شناسه کاربر تلگرام خود را در پیکربندی (های) خود استفاده کند.\r\n\r\nشناسه کاربری شما: {{ .TgUserID }}" "chooseClient" = "یک مشتری برای ورودی {{ .Inbound }} انتخاب کنید" "chooseInbound" = "یک ورودی انتخاب کنید" ================================================ FILE: web/translation/translate.id_ID.toml ================================================ "username" = "Nama Pengguna" "password" = "Kata Sandi" "login" = "Masuk" "confirm" = "Konfirmasi" "cancel" = "Batal" "close" = "Tutup" "create" = "Buat" "update" = "Perbarui" "copy" = "Salin" "copied" = "Tersalin" "download" = "Unduh" "remark" = "Catatan" "enable" = "Aktifkan" "protocol" = "Protokol" "search" = "Cari" "filter" = "Filter" "loading" = "Memuat..." "second" = "Detik" "minute" = "Menit" "hour" = "Jam" "day" = "Hari" "check" = "Centang" "indefinite" = "Tak Terbatas" "unlimited" = "Tanpa Batas" "none" = "None" "qrCode" = "Kode QR" "info" = "Informasi Lebih Lanjut" "edit" = "Edit" "delete" = "Hapus" "reset" = "Reset" "noData" = "Tidak ada data." "copySuccess" = "Berhasil Disalin" "sure" = "Yakin" "encryption" = "Enkripsi" "useIPv4ForHost" = "Gunakan IPv4 untuk host" "transmission" = "Transmisi" "host" = "Host" "path" = "Jalur" "camouflage" = "Obfuscation" "status" = "Status" "enabled" = "Aktif" "disabled" = "Nonaktif" "depleted" = "Habis" "depletingSoon" = "Akan Habis" "offline" = "Offline" "online" = "Online" "domainName" = "Nama Domain" "monitor" = "IP Pemantauan" "certificate" = "Sertifikat Digital" "fail" = "Gagal" "comment" = "Komentar" "success" = "Berhasil" "lastOnline" = "Terakhir online" "getVersion" = "Dapatkan Versi" "install" = "Instal" "clients" = "Klien" "usage" = "Penggunaan" "twoFactorCode" = "Kode" "remained" = "Tersisa" "security" = "Keamanan" "secAlertTitle" = "Peringatan keamanan" "secAlertSsl" = "Koneksi ini tidak aman. Harap hindari memasukkan informasi sensitif sampai TLS diaktifkan untuk perlindungan data." "secAlertConf" = "Beberapa pengaturan rentan terhadap serangan. Disarankan untuk memperkuat protokol keamanan guna mencegah pelanggaran potensial." "secAlertSSL" = "Panel kekurangan koneksi yang aman. Harap instal sertifikat TLS untuk perlindungan data." "secAlertPanelPort" = "Port default panel rentan. Harap konfigurasi port acak atau tertentu." "secAlertPanelURI" = "Jalur URI default panel tidak aman. Harap konfigurasi jalur URI kompleks." "secAlertSubURI" = "Jalur URI default langganan tidak aman. Harap konfigurasi jalur URI kompleks." "secAlertSubJsonURI" = "Jalur URI default JSON langganan tidak aman. Harap konfigurasikan jalur URI kompleks." "emptyDnsDesc" = "Tidak ada server DNS yang ditambahkan." "emptyFakeDnsDesc" = "Tidak ada server Fake DNS yang ditambahkan." "emptyBalancersDesc" = "Tidak ada penyeimbang yang ditambahkan." "emptyReverseDesc" = "Tidak ada proxy terbalik yang ditambahkan." "somethingWentWrong" = "Terjadi kesalahan" [subscription] "title" = "Info langganan" "subId" = "ID langganan" "status" = "Status" "downloaded" = "Diunduh" "uploaded" = "Diunggah" "expiry" = "Kedaluwarsa" "totalQuota" = "Kuota total" "individualLinks" = "Tautan individual" "active" = "Aktif" "inactive" = "Nonaktif" "unlimited" = "Tanpa batas" "noExpiry" = "Tanpa kedaluwarsa" [menu] "theme" = "Tema" "dark" = "Gelap" "ultraDark" = "Sangat Gelap" "dashboard" = "Ikhtisar" "inbounds" = "Masuk" "settings" = "Pengaturan Panel" "xray" = "Konfigurasi Xray" "logout" = "Keluar" "link" = "Kelola" [pages.login] "hello" = "Halo" "title" = "Selamat Datang" "loginAgain" = "Sesi Anda telah berakhir, harap masuk kembali" [pages.login.toasts] "invalidFormData" = "Format data input tidak valid." "emptyUsername" = "Nama Pengguna diperlukan" "emptyPassword" = "Kata Sandi diperlukan" "wrongUsernameOrPassword" = "Username, kata sandi, atau kode dua faktor tidak valid." "successLogin" = "Anda telah berhasil masuk ke akun Anda." [pages.index] "title" = "Ikhtisar" "cpu" = "CPU" "logicalProcessors" = "Prosesor logis" "frequency" = "Frekuensi" "swap" = "Swap" "storage" = "Penyimpanan" "memory" = "RAM" "threads" = "Thread" "xrayStatus" = "Xray" "stopXray" = "Stop" "restartXray" = "Restart" "xraySwitch" = "Versi" "xraySwitchClick" = "Pilih versi yang ingin Anda pindah." "xraySwitchClickDesk" = "Pilih dengan hati-hati, karena versi yang lebih lama mungkin tidak kompatibel dengan konfigurasi saat ini." "xrayStatusUnknown" = "Tidak diketahui" "xrayStatusRunning" = "Berjalan" "xrayStatusStop" = "Berhenti" "xrayStatusError" = "Kesalahan" "xrayErrorPopoverTitle" = "Terjadi kesalahan saat menjalankan Xray" "operationHours" = "Waktu Aktif" "systemLoad" = "Beban Sistem" "systemLoadDesc" = "Rata-rata beban sistem selama 1, 5, dan 15 menit terakhir" "connectionCount" = "Statistik Koneksi" "ipAddresses" = "Alamat IP" "toggleIpVisibility" = "Alihkan visibilitas IP" "overallSpeed" = "Kecepatan keseluruhan" "upload" = "Unggah" "download" = "Unduh" "totalData" = "Total data" "sent" = "Dikirim" "received" = "Diterima" "documentation" = "Dokumentasi" "xraySwitchVersionDialog" = "Apakah Anda yakin ingin mengubah versi Xray?" "xraySwitchVersionDialogDesc" = "Ini akan mengubah versi Xray ke #version#." "xraySwitchVersionPopover" = "Xray berhasil diperbarui" "geofileUpdateDialog" = "Apakah Anda yakin ingin memperbarui geofile?" "geofileUpdateDialogDesc" = "Ini akan memperbarui file #filename#." "geofilesUpdateDialogDesc" = "Ini akan memperbarui semua berkas." "geofilesUpdateAll" = "Perbarui semua" "geofileUpdatePopover" = "Geofile berhasil diperbarui" "dontRefresh" = "Instalasi sedang berlangsung, harap jangan menyegarkan halaman ini" "logs" = "Log" "config" = "Konfigurasi" "backup" = "Cadangan" "backupTitle" = "Cadangan & Pulihkan Database" "exportDatabase" = "Cadangkan" "exportDatabaseDesc" = "Klik untuk mengunduh file .db yang berisi cadangan dari database Anda saat ini ke perangkat Anda." "importDatabase" = "Pulihkan" "importDatabaseDesc" = "Klik untuk memilih dan mengunggah file .db dari perangkat Anda untuk memulihkan database dari cadangan." "importDatabaseSuccess" = "Database berhasil diimpor" "importDatabaseError" = "Terjadi kesalahan saat mengimpor database" "readDatabaseError" = "Terjadi kesalahan saat membaca database" "getDatabaseError" = "Terjadi kesalahan saat mengambil database" "getConfigError" = "Terjadi kesalahan saat mengambil file konfigurasi" [pages.inbounds] "allTimeTraffic" = "Total Lalu Lintas" "allTimeTrafficUsage" = "Total Penggunaan Sepanjang Waktu" "title" = "Masuk" "totalDownUp" = "Total Terkirim/Diterima" "totalUsage" = "Penggunaan Total" "inboundCount" = "Total Masuk" "operate" = "Menu" "enable" = "Aktifkan" "remark" = "Catatan" "protocol" = "Protokol" "port" = "Port" "portMap" = "Port Mapping" "traffic" = "Traffic" "details" = "Rincian" "transportConfig" = "Transport" "expireDate" = "Durasi" "createdAt" = "Dibuat" "updatedAt" = "Diperbarui" "resetTraffic" = "Reset Traffic" "addInbound" = "Tambahkan Masuk" "generalActions" = "Tindakan Umum" "autoRefresh" = "Pembaruan otomatis" "autoRefreshInterval" = "Interval" "modifyInbound" = "Ubah Masuk" "deleteInbound" = "Hapus Masuk" "deleteInboundContent" = "Apakah Anda yakin ingin menghapus masuk?" "deleteClient" = "Hapus Klien" "deleteClientContent" = "Apakah Anda yakin ingin menghapus klien?" "resetTrafficContent" = "Apakah Anda yakin ingin mereset traffic?" "copyLink" = "Salin URL" "address" = "Alamat" "network" = "Jaringan" "destinationPort" = "Port Tujuan" "targetAddress" = "Alamat Target" "monitorDesc" = "Biarkan kosong untuk mendengarkan semua IP" "meansNoLimit" = "= Unlimited. (unit: GB)" "totalFlow" = "Total Aliran" "leaveBlankToNeverExpire" = "Biarkan kosong untuk tidak pernah kedaluwarsa" "noRecommendKeepDefault" = "Disarankan untuk tetap menggunakan pengaturan default" "certificatePath" = "Path Berkas" "certificateContent" = "Konten Berkas" "publicKey" = "Kunci Publik" "privatekey" = "Kunci Pribadi" "clickOnQRcode" = "Klik pada Kode QR untuk Menyalin" "client" = "Klien" "export" = "Ekspor Semua URL" "clone" = "Duplikat" "cloneInbound" = "Duplikat" "cloneInboundContent" = "Semua pengaturan masuk ini, kecuali Port, Listening IP, dan Klien, akan diterapkan pada duplikat." "cloneInboundOk" = "Duplikat" "resetAllTraffic" = "Reset Semua Traffic Masuk" "resetAllTrafficTitle" = "Reset Semua Traffic Masuk" "resetAllTrafficContent" = "Apakah Anda yakin ingin mereset traffic semua masuk?" "resetInboundClientTraffics" = "Reset Traffic Klien Masuk" "resetInboundClientTrafficTitle" = "Reset Traffic Klien Masuk" "resetInboundClientTrafficContent" = "Apakah Anda yakin ingin mereset traffic klien masuk ini?" "resetAllClientTraffics" = "Reset Traffic Semua Klien" "resetAllClientTrafficTitle" = "Reset Traffic Semua Klien" "resetAllClientTrafficContent" = "Apakah Anda yakin ingin mereset traffic semua klien?" "delDepletedClients" = "Hapus Klien Habis" "delDepletedClientsTitle" = "Hapus Klien Habis" "delDepletedClientsContent" = "Apakah Anda yakin ingin menghapus semua klien yang habis?" "email" = "Email" "emailDesc" = "Harap berikan alamat email yang unik." "IPLimit" = "Batas IP" "IPLimitDesc" = "Menonaktifkan masuk jika jumlah melebihi nilai yang ditetapkan. (0 = nonaktif)" "IPLimitlog" = "Log IP" "IPLimitlogDesc" = "Log histori IP. (untuk mengaktifkan masuk setelah menonaktifkan, hapus log)" "IPLimitlogclear" = "Hapus Log" "setDefaultCert" = "Atur Sertifikat dari Panel" "telegramDesc" = "Harap berikan ID Obrolan Telegram. (gunakan perintah '/id' di bot) atau (@userinfobot)" "subscriptionDesc" = "Untuk menemukan URL langganan Anda, buka 'Rincian'. Selain itu, Anda dapat menggunakan nama yang sama untuk beberapa klien." "info" = "Info" "same" = "Sama" "inboundData" = "Data Masuk" "exportInbound" = "Ekspor Masuk" "import" = "Impor" "importInbound" = "Impor Masuk" "periodicTrafficResetTitle" = "Reset Trafik Berkala" "periodicTrafficResetDesc" = "Reset otomatis penghitung trafik pada interval tertentu" "lastReset" = "Reset Terakhir" [pages.client] "add" = "Tambah Klien" "edit" = "Edit Klien" "submitAdd" = "Tambah Klien" "submitEdit" = "Simpan Perubahan" "clientCount" = "Jumlah Klien" "bulk" = "Tambahkan Massal" "method" = "Metode" "first" = "Pertama" "last" = "Terakhir" "prefix" = "Awalan" "postfix" = "Akhiran" "delayedStart" = "Mulai Awal" "expireDays" = "Durasi" "days" = "Hari" "renew" = "Perpanjang Otomatis" "renewDesc" = "Perpanjangan otomatis setelah kedaluwarsa. (0 = nonaktif)(unit: hari)" [pages.inbounds.periodicTrafficReset] "never" = "Tidak Pernah" "daily" = "Harian" "weekly" = "Mingguan" "monthly" = "Bulanan" [pages.inbounds.toasts] "obtain" = "Dapatkan" "updateSuccess" = "Pembaruan berhasil" "logCleanSuccess" = "Log telah dibersihkan" "inboundsUpdateSuccess" = "Inbound berhasil diperbarui" "inboundUpdateSuccess" = "Inbound berhasil diperbarui" "inboundCreateSuccess" = "Inbound berhasil dibuat" "inboundDeleteSuccess" = "Inbound berhasil dihapus" "inboundClientAddSuccess" = "Klien inbound telah ditambahkan" "inboundClientDeleteSuccess" = "Klien inbound telah dihapus" "inboundClientUpdateSuccess" = "Klien inbound telah diperbarui" "delDepletedClientsSuccess" = "Semua klien yang habis telah dihapus" "resetAllClientTrafficSuccess" = "Semua lalu lintas klien telah direset" "resetAllTrafficSuccess" = "Semua lalu lintas telah direset" "resetInboundClientTrafficSuccess" = "Lalu lintas telah direset" "trafficGetError" = "Gagal mendapatkan data lalu lintas" "getNewX25519CertError" = "Terjadi kesalahan saat mendapatkan sertifikat X25519." "getNewmldsa65Error" = "Terjadi kesalahan saat mendapatkan sertifikat mldsa65." "getNewVlessEncError" = "Terjadi kesalahan saat mendapatkan sertifikat VlessEnc." [pages.inbounds.stream.general] "request" = "Permintaan" "response" = "Respons" "name" = "Nama" "value" = "Nilai" [pages.inbounds.stream.tcp] "version" = "Versi" "method" = "Metode" "path" = "Path" "status" = "Status" "statusDescription" = "Deskripsi Status" "requestHeader" = "Header Permintaan" "responseHeader" = "Header Respons" [pages.settings] "title" = "Pengaturan Panel" "save" = "Simpan" "infoDesc" = "Setiap perubahan yang dibuat di sini perlu disimpan. Harap restart panel untuk menerapkan perubahan." "restartPanel" = "Restart Panel" "restartPanelDesc" = "Apakah Anda yakin ingin merestart panel? Jika Anda tidak dapat mengakses panel setelah merestart, lihat info log panel di server." "restartPanelSuccess" = "Panel berhasil dimulai ulang" "actions" = "Tindakan" "resetDefaultConfig" = "Reset ke Default" "panelSettings" = "Umum" "securitySettings" = "Otentikasi" "TGBotSettings" = "Bot Telegram" "panelListeningIP" = "IP Pendengar" "panelListeningIPDesc" = "Alamat IP untuk panel web. (biarkan kosong untuk mendengarkan semua IP)" "panelListeningDomain" = "Domain Pendengar" "panelListeningDomainDesc" = "Nama domain untuk panel web. (biarkan kosong untuk mendengarkan semua domain dan IP)" "panelPort" = "Port Pendengar" "panelPortDesc" = "Nomor port untuk panel web. (harus menjadi port yang tidak digunakan)" "publicKeyPath" = "Path Kunci Publik" "publicKeyPathDesc" = "Path berkas kunci publik untuk panel web. (dimulai dengan ‘/‘)" "privateKeyPath" = "Path Kunci Privat" "privateKeyPathDesc" = "Path berkas kunci privat untuk panel web. (dimulai dengan ‘/‘)" "panelUrlPath" = "URI Path" "panelUrlPathDesc" = "URI path untuk panel web. (dimulai dengan ‘/‘ dan diakhiri dengan ‘/‘)" "pageSize" = "Ukuran Halaman" "pageSizeDesc" = "Tentukan ukuran halaman untuk tabel masuk. (0 = nonaktif)" "remarkModel" = "Model Catatan & Karakter Pemisah" "datepicker" = "Jenis Kalender" "datepickerPlaceholder" = "Pilih tanggal" "datepickerDescription" = "Tugas terjadwal akan berjalan berdasarkan kalender ini." "sampleRemark" = "Contoh Catatan" "oldUsername" = "Username Saat Ini" "currentPassword" = "Kata Sandi Saat Ini" "newUsername" = "Username Baru" "newPassword" = "Kata Sandi Baru" "telegramBotEnable" = "Aktifkan Bot Telegram" "telegramBotEnableDesc" = "Mengaktifkan bot Telegram." "telegramToken" = "Token Telegram" "telegramTokenDesc" = "Token bot Telegram yang diperoleh dari '@BotFather'." "telegramProxy" = "Proxy SOCKS" "telegramProxyDesc" = "Mengaktifkan proxy SOCKS5 untuk terhubung ke Telegram. (sesuaikan pengaturan sesuai panduan)" "telegramAPIServer" = "Telegram API Server" "telegramAPIServerDesc" = "Server API Telegram yang akan digunakan. Biarkan kosong untuk menggunakan server default." "telegramChatId" = "ID Obrolan Admin" "telegramChatIdDesc" = "ID Obrolan Admin Telegram. (dipisahkan koma)(dapatkan di sini @userinfobot) atau (gunakan perintah '/id' di bot)" "telegramNotifyTime" = "Waktu Notifikasi" "telegramNotifyTimeDesc" = "Waktu notifikasi bot Telegram yang diatur untuk laporan berkala. (gunakan format waktu crontab)" "tgNotifyBackup" = "Cadangan Database" "tgNotifyBackupDesc" = "Kirim berkas cadangan database dengan laporan." "tgNotifyLogin" = "Notifikasi Login" "tgNotifyLoginDesc" = "Dapatkan notifikasi tentang username, alamat IP, dan waktu setiap kali seseorang mencoba masuk ke panel web Anda." "sessionMaxAge" = "Durasi Sesi" "sessionMaxAgeDesc" = "Durasi di mana Anda dapat tetap masuk. (unit: menit)" "expireTimeDiff" = "Notifikasi Tanggal Kedaluwarsa" "expireTimeDiffDesc" = "Dapatkan notifikasi tentang tanggal kedaluwarsa saat mencapai ambang batas ini. (unit: hari)" "trafficDiff" = "Notifikasi Batas Traffic" "trafficDiffDesc" = "Dapatkan notifikasi tentang batas traffic saat mencapai ambang batas ini. (unit: GB)" "tgNotifyCpu" = "Notifikasi Beban CPU" "tgNotifyCpuDesc" = "Dapatkan notifikasi jika beban CPU melebihi ambang batas ini. (unit: %)" "timeZone" = "Zone Waktu" "timeZoneDesc" = "Tugas terjadwal akan berjalan berdasarkan zona waktu ini." "subSettings" = "Langganan" "subEnable" = "Aktifkan Layanan Langganan" "subEnableDesc" = "Mengaktifkan layanan langganan." "subJsonEnable" = "Aktifkan/Nonaktifkan endpoint langganan JSON secara mandiri." "subTitle" = "Judul Langganan" "subTitleDesc" = "Judul yang ditampilkan di klien VPN" "subSupportUrl" = "URL Dukungan" "subSupportUrlDesc" = "Tautan dukungan teknis yang ditampilkan di klien VPN" "subProfileUrl" = "URL Profil" "subProfileUrlDesc" = "Tautan ke situs web Anda yang ditampilkan di klien VPN" "subAnnounce" = "Pengumuman" "subAnnounceDesc" = "Teks pengumuman yang ditampilkan di klien VPN" "subEnableRouting" = "Aktifkan perutean" "subEnableRoutingDesc" = "Pengaturan global untuk mengaktifkan perutean (routing) di klien VPN. (Hanya untuk Happ)" "subRoutingRules" = "Aturan routing" "subRoutingRulesDesc" = "Aturan routing global untuk klien VPN. (Hanya untuk Happ)" "subListen" = "IP Pendengar" "subListenDesc" = "Alamat IP untuk layanan langganan. (biarkan kosong untuk mendengarkan semua IP)" "subPort" = "Port Pendengar" "subPortDesc" = "Nomor port untuk layanan langganan. (harus menjadi port yang tidak digunakan)" "subCertPath" = "Path Kunci Publik" "subCertPathDesc" = "Path berkas kunci publik untuk layanan langganan. (dimulai dengan ‘/‘)" "subKeyPath" = "Path Kunci Privat" "subKeyPathDesc" = "Path berkas kunci privat untuk layanan langganan. (dimulai dengan ‘/‘)" "subPath" = "URI Path" "subPathDesc" = "URI path untuk layanan langganan. (dimulai dengan ‘/‘ dan diakhiri dengan ‘/‘)" "subDomain" = "Domain Pendengar" "subDomainDesc" = "Nama domain untuk layanan langganan. (biarkan kosong untuk mendengarkan semua domain dan IP)" "subUpdates" = "Interval Pembaruan" "subUpdatesDesc" = "Interval pembaruan URL langganan dalam aplikasi klien. (unit: jam)" "subEncrypt" = "Encode" "subEncryptDesc" = "Konten yang dikembalikan dari layanan langganan akan dienkripsi Base64." "subShowInfo" = "Tampilkan Info Penggunaan" "subShowInfoDesc" = "Sisa traffic dan tanggal akan ditampilkan di aplikasi klien." "subURI" = "URI Proxy Terbalik" "subURIDesc" = "Path URI dari URL langganan untuk digunakan di belakang proxy." "externalTrafficInformEnable" = "Informasikan API eksternal pada setiap pembaruan lalu lintas." "externalTrafficInformEnableDesc" = "Inform external API on every traffic update." "externalTrafficInformURI" = "Lalu Lintas Eksternal Menginformasikan URI" "externalTrafficInformURIDesc" = "Pembaruan lalu lintas dikirim ke URI ini." "fragment" = "Fragmentasi" "fragmentDesc" = "Aktifkan fragmentasi untuk paket hello TLS" "fragmentSett" = "Pengaturan Fragmentasi" "noisesDesc" = "Aktifkan Noises." "noisesSett" = "Pengaturan Noises" "mux" = "Mux" "muxDesc" = "Mengirimkan beberapa aliran data independen dalam aliran data yang sudah ada." "muxSett" = "Pengaturan Mux" "direct" = "Koneksi langsung" "directDesc" = "Secara langsung membuat koneksi dengan domain atau rentang IP negara tertentu." "notifications" = "Notifikasi" "certs" = "Sertifikat" "externalTraffic" = "Lalu Lintas Eksternal" "dateAndTime" = "Tanggal dan Waktu" "proxyAndServer" = "Proxy dan Server" "intervals" = "Interval" "information" = "Informasi" "language" = "Bahasa" "telegramBotLanguage" = "Bahasa Bot Telegram" [pages.xray] "title" = "Konfigurasi Xray" "save" = "Simpan" "restart" = "Restart Xray" "restartSuccess" = "Xray berhasil diluncurkan ulang" "stopSuccess" = "Xray telah berhasil dihentikan" "restartError" = "Terjadi kesalahan saat memulai ulang Xray." "stopError" = "Terjadi kesalahan saat menghentikan Xray." "basicTemplate" = "Dasar" "advancedTemplate" = "Lanjutan" "generalConfigs" = "Strategi Umum" "generalConfigsDesc" = "Opsi ini akan menentukan penyesuaian strategi umum." "logConfigs" = "Catatan" "logConfigsDesc" = "Log dapat mempengaruhi efisiensi server Anda. Disarankan untuk mengaktifkannya dengan bijak hanya jika diperlukan" "blockConfigsDesc" = "Opsi ini akan memblokir lalu lintas berdasarkan protokol dan situs web yang diminta." "basicRouting" = "Perutean Dasar" "blockConnectionsConfigsDesc" = "Opsi ini akan memblokir lalu lintas berdasarkan negara yang diminta." "directConnectionsConfigsDesc" = "Koneksi langsung memastikan bahwa lalu lintas tertentu tidak dialihkan melalui server lain." "blockips" = "Blokir IP" "blockdomains" = "Blokir Domain" "directips" = "IP Langsung" "directdomains" = "Domain Langsung" "ipv4Routing" = "Perutean IPv4" "ipv4RoutingDesc" = "Opsi ini akan mengalihkan lalu lintas berdasarkan tujuan tertentu melalui IPv4." "warpRouting" = "Perutean WARP" "warpRoutingDesc" = "Opsi ini akan mengalihkan lalu lintas berdasarkan tujuan tertentu melalui WARP." "Template" = "Template Konfigurasi Xray Lanjutan" "TemplateDesc" = "File konfigurasi Xray akhir akan dibuat berdasarkan template ini." "FreedomStrategy" = "Strategi Protokol Freedom" "FreedomStrategyDesc" = "Atur strategi output untuk jaringan dalam Protokol Freedom." "RoutingStrategy" = "Strategi Pengalihan Keseluruhan" "RoutingStrategyDesc" = "Atur strategi pengalihan lalu lintas keseluruhan untuk menyelesaikan semua permintaan." "outboundTestUrl" = "URL tes outbound" "outboundTestUrlDesc" = "URL yang digunakan saat menguji konektivitas outbound" "Torrent" = "Blokir Protokol BitTorrent" "Inbounds" = "Masuk" "InboundsDesc" = "Menerima klien tertentu." "Outbounds" = "Keluar" "Balancers" = "Penyeimbang" "OutboundsDesc" = "Atur jalur lalu lintas keluar." "Routings" = "Aturan Pengalihan" "RoutingsDesc" = "Prioritas setiap aturan penting!" "completeTemplate" = "Semua" "logLevel" = "Tingkat Log" "logLevelDesc" = "Tingkat log untuk log kesalahan, menunjukkan informasi yang perlu dicatat." "accessLog" = "Log Akses" "accessLogDesc" = "Jalur file untuk log akses. Nilai khusus 'tidak ada' menonaktifkan log akses" "errorLog" = "Catatan eror" "errorLogDesc" = "Jalur file untuk log kesalahan. Nilai khusus 'tidak ada' menonaktifkan log kesalahan" "dnsLog" = "Log DNS" "dnsLogDesc" = "Apakah akan mengaktifkan log kueri DNS" "maskAddress" = "Alamat Masker" "maskAddressDesc" = "Masker alamat IP, ketika diaktifkan, akan secara otomatis mengganti alamat IP yang muncul di log." "statistics" = "Statistik" "statsInboundUplink" = "Statistik Unggah Masuk" "statsInboundUplinkDesc" = "Mengaktifkan pengumpulan statistik untuk lalu lintas unggah dari semua proxy masuk." "statsInboundDownlink" = "Statistik Unduh Masuk" "statsInboundDownlinkDesc" = "Mengaktifkan pengumpulan statistik untuk lalu lintas unduh dari semua proxy masuk." "statsOutboundUplink" = "Statistik Unggah Keluar" "statsOutboundUplinkDesc" = "Mengaktifkan pengumpulan statistik untuk lalu lintas unggah dari semua proxy keluar." "statsOutboundDownlink" = "Statistik Unduh Keluar" "statsOutboundDownlinkDesc" = "Mengaktifkan pengumpulan statistik untuk lalu lintas unduh dari semua proxy keluar." [pages.xray.rules] "first" = "Pertama" "last" = "Terakhir" "up" = "Naik" "down" = "Turun" "source" = "Sumber" "dest" = "Tujuan" "inbound" = "Masuk" "outbound" = "Keluar" "balancer" = "Pengimbang" "info" = "Info" "add" = "Tambahkan Aturan" "edit" = "Edit Aturan" "useComma" = "Item yang dipisahkan koma" [pages.xray.outbound] "addOutbound" = "Tambahkan Keluar" "addReverse" = "Tambahkan Revers" "editOutbound" = "Edit Keluar" "editReverse" = "Edit Revers" "tag" = "Tag" "tagDesc" = "Tag Unik" "address" = "Alamat" "reverse" = "Revers" "domain" = "Domain" "type" = "Tipe" "bridge" = "Jembatan" "portal" = "Portal" "link" = "Tautan" "intercon" = "Interkoneksi" "settings" = "Pengaturan" "accountInfo" = "Informasi Akun" "outboundStatus" = "Status Keluar" "sendThrough" = "Kirim Melalui" "test" = "Tes" "testResult" = "Hasil Tes" "testing" = "Menguji koneksi..." "testSuccess" = "Tes berhasil" "testFailed" = "Tes gagal" "testError" = "Gagal menguji outbound" [pages.xray.balancer] "addBalancer" = "Tambahkan Penyeimbang" "editBalancer" = "Sunting Penyeimbang" "balancerStrategy" = "Strategi" "balancerSelectors" = "Penyeleksi" "tag" = "Menandai" "tagDesc" = "Label Unik" "balancerDesc" = "BalancerTag dan outboundTag tidak dapat digunakan secara bersamaan. Jika digunakan secara bersamaan, hanya outboundTag yang akan berfungsi." [pages.xray.wireguard] "secretKey" = "Kunci Rahasia" "publicKey" = "Kunci Publik" "allowedIPs" = "IP yang Diizinkan" "endpoint" = "Titik Akhir" "psk" = "Kunci Pra-Bagi" "domainStrategy" = "Strategi Domain" [pages.xray.tun] "nameDesc" = "Nama antarmuka TUN. Standar adalah 'xray0'" "mtuDesc" = "Unit Transmisi Maksimum. Ukuran maksimum paket data. Standar adalah 1500" "userLevel" = "Level Pengguna" "userLevelDesc" = "Semua koneksi yang dibuat melalui inbound ini akan menggunakan level pengguna ini. Standar adalah 0" [pages.xray.dns] "enable" = "Aktifkan DNS" "enableDesc" = "Aktifkan server DNS bawaan" "tag" = "Tanda DNS Masuk" "tagDesc" = "Tanda ini akan tersedia sebagai tanda masuk dalam aturan penataan." "clientIp" = "IP Klien" "clientIpDesc" = "Digunakan untuk memberi tahu server tentang lokasi IP yang ditentukan selama kueri DNS" "disableCache" = "Nonaktifkan cache" "disableCacheDesc" = "Menonaktifkan caching DNS" "disableFallback" = "Nonaktifkan Fallback" "disableFallbackDesc" = "Menonaktifkan kueri DNS fallback" "disableFallbackIfMatch" = "Nonaktifkan Fallback Jika Cocok" "disableFallbackIfMatchDesc" = "Menonaktifkan kueri DNS fallback ketika daftar domain yang cocok dari server DNS terpenuhi" "enableParallelQuery" = "Aktifkan Kueri Paralel" "enableParallelQueryDesc" = "Aktifkan kueri DNS paralel ke beberapa server untuk resolusi yang lebih cepat" "strategy" = "Strategi Kueri" "strategyDesc" = "Strategi keseluruhan untuk menyelesaikan nama domain" "add" = "Tambahkan Server" "edit" = "Sunting Server" "domains" = "Domains" "expectIPs" = "IP yang Diharapkan" "unexpectIPs" = "IP tak terduga" "useSystemHosts" = "Gunakan Hosts Sistem" "useSystemHostsDesc" = "Gunakan file hosts dari sistem yang terinstal" "usePreset" = "Gunakan templat" "dnsPresetTitle" = "Templat DNS" "dnsPresetFamily" = "Keluarga" [pages.xray.fakedns] "add" = "Tambahkan DNS Palsu" "edit" = "Edit DNS Palsu" "ipPool" = "Subnet Kumpulan IP" "poolSize" = "Ukuran Kolam" [pages.settings.security] "admin" = "Kredensial admin" "twoFactor" = "Autentikasi dua faktor" "twoFactorEnable" = "Aktifkan 2FA" "twoFactorEnableDesc" = "Menambahkan lapisan autentikasi tambahan untuk keamanan lebih." "twoFactorModalSetTitle" = "Aktifkan autentikasi dua faktor" "twoFactorModalDeleteTitle" = "Nonaktifkan autentikasi dua faktor" "twoFactorModalSteps" = "Untuk menyiapkan autentikasi dua faktor, lakukan beberapa langkah:" "twoFactorModalFirstStep" = "1. Pindai kode QR ini di aplikasi autentikasi atau salin token di dekat kode QR dan tempelkan ke aplikasi" "twoFactorModalSecondStep" = "2. Masukkan kode dari aplikasi" "twoFactorModalRemoveStep" = "Masukkan kode dari aplikasi untuk menghapus autentikasi dua faktor." "twoFactorModalChangeCredentialsTitle" = "Ubah kredensial" "twoFactorModalChangeCredentialsStep" = "Masukkan kode dari aplikasi untuk mengubah kredensial administrator." "twoFactorModalSetSuccess" = "Autentikasi dua faktor telah berhasil dibuat" "twoFactorModalDeleteSuccess" = "Autentikasi dua faktor telah berhasil dihapus" "twoFactorModalError" = "Kode salah" [pages.settings.toasts] "modifySettings" = "Parameter telah diubah." "getSettings" = "Terjadi kesalahan saat mengambil parameter." "modifyUserError" = "Terjadi kesalahan saat mengubah kredensial administrator." "modifyUser" = "Anda telah berhasil mengubah kredensial administrator." "originalUserPassIncorrect" = "Username atau password saat ini tidak valid" "userPassMustBeNotEmpty" = "Username dan password baru tidak boleh kosong" "getOutboundTrafficError" = "Gagal mendapatkan lalu lintas keluar" "resetOutboundTrafficError" = "Gagal mereset lalu lintas keluar" [tgbot] "keyboardClosed" = "❌ Keyboard ditutup!" "noResult" = "❗ Tidak ada hasil!" "noQuery" = "❌ Kueri tidak ditemukan! Silakan gunakan perintah lagi!" "wentWrong" = "❌ Terjadi kesalahan!" "noIpRecord" = "❗ Tidak ada Catatan IP!" "noInbounds" = "❗ Tidak ada inbound yang ditemukan!" "unlimited" = "♾ Tidak terbatas (Reset)" "add" = "Tambah" "month" = "Bulan" "months" = "Bulan" "day" = "Hari" "days" = "Hari" "hours" = "Jam" "minutes" = "Menit" "unknown" = "Tidak diketahui" "inbounds" = "Inbound" "clients" = "Klien" "offline" = "🔴 Offline" "online" = "🟢 Online" [tgbot.commands] "unknown" = "❗ Perintah tidak dikenal." "pleaseChoose" = "👇 Harap pilih:\r\n" "help" = "🤖 Selamat datang di bot ini! Ini dirancang untuk menyediakan data tertentu dari panel web dan memungkinkan Anda melakukan modifikasi sesuai kebutuhan.\r\n\r\n" "start" = "👋 Halo {{ .Firstname }}.\r\n" "welcome" = "🤖 Selamat datang di {{.Hostname }} bot managemen.\r\n" "status" = "✅ Bot dalam keadaan baik!" "usage" = "❗ Harap berikan teks untuk mencari!" "getID" = "🆔 ID Anda: {{ .ID }}" "helpAdminCommands" = "Untuk memulai ulang Xray Core:\r\n/restart\r\n\r\nUntuk mencari email klien:\r\n/usage [Email]\r\n\r\nUntuk mencari inbound (dengan statistik klien):\r\n/inbound [Catatan]\r\n\r\nID Obrolan Telegram:\r\n/id" "helpClientCommands" = "Untuk mencari statistik, gunakan perintah berikut:\r\n/usage [Email]\r\n\r\nID Obrolan Telegram:\r\n/id" "restartUsage" = "\r\n\r\n/restart" "restartSuccess" = "✅ Operasi berhasil!" "restartFailed" = "❗ Kesalahan dalam operasi.\r\n\r\nError: {{ .Error }}." "xrayNotRunning" = "❗ Xray Core tidak berjalan." "startDesc" = "Tampilkan menu utama" "helpDesc" = "Bantuan bot" "statusDesc" = "Periksa status bot" "idDesc" = "Tampilkan ID Telegram Anda" [tgbot.messages] "cpuThreshold" = "🔴 Beban CPU {{ .Percent }}% melebihi batas {{ .Threshold }}%" "selectUserFailed" = "❌ Kesalahan dalam pemilihan pengguna!" "userSaved" = "✅ Pengguna Telegram tersimpan." "loginSuccess" = "✅ Berhasil masuk ke panel.\r\n" "loginFailed" = "❗️ Gagal masuk ke panel.\r\n" "2faFailed" = "2FA Gagal" "report" = "🕰 Laporan Terjadwal: {{ .RunTime }}\r\n" "datetime" = "⏰ Tanggal & Waktu: {{ .DateTime }}\r\n" "hostname" = "💻 Host: {{ .Hostname }}\r\n" "version" = "🚀 Versi 3X-UI: {{ .Version }}\r\n" "xrayVersion" = "📡 Versi Xray: {{ .XrayVersion }}\r\n" "ipv6" = "🌐 IPv6: {{ .IPv6 }}\r\n" "ipv4" = "🌐 IPv4: {{ .IPv4 }}\r\n" "ip" = "🌐 IP: {{ .IP }}\r\n" "ips" = "🔢 IP:\r\n{{ .IPs }}\r\n" "serverUpTime" = "⏳ Waktu Aktif: {{ .UpTime }} {{ .Unit }}\r\n" "serverLoad" = "📈 Beban Sistem: {{ .Load1 }}, {{ .Load2 }}, {{ .Load3 }}\r\n" "serverMemory" = "📋 RAM: {{ .Current }}/{{ .Total }}\r\n" "tcpCount" = "🔹 TCP: {{ .Count }}\r\n" "udpCount" = "🔸 UDP: {{ .Count }}\r\n" "traffic" = "🚦 Lalu Lintas: {{ .Total }} (↑{{ .Upload }},↓{{ .Download }})\r\n" "xrayStatus" = "ℹ️ Status: {{ .State }}\r\n" "username" = "👤 Nama Pengguna: {{ .Username }}\r\n" "password" = "👤 Kata Sandi: {{ .Password }}\r\n" "time" = "⏰ Waktu: {{ .Time }}\r\n" "inbound" = "📍 Inbound: {{ .Remark }}\r\n" "port" = "🔌 Port: {{ .Port }}\r\n" "expire" = "📅 Tanggal Kadaluarsa: {{ .Time }}\r\n" "expireIn" = "📅 Kadaluarsa Dalam: {{ .Time }}\r\n" "active" = "💡 Aktif: {{ .Enable }}\r\n" "enabled" = "🚨 Diaktifkan: {{ .Enable }}\r\n" "online" = "🌐 Status Koneksi: {{ .Status }}\r\n" "lastOnline" = "🔙 Terakhir online: {{ .Time }}\r\n" "email" = "📧 Email: {{ .Email }}\r\n" "upload" = "🔼 Unggah: ↑{{ .Upload }}\r\n" "download" = "🔽 Unduh: ↓{{ .Download }}\r\n" "total" = "📊 Total: ↑↓{{ .UpDown }} / {{ .Total }}\r\n" "TGUser" = "👤 Pengguna Telegram: {{ .TelegramID }}\r\n" "exhaustedMsg" = "🚨 Habis {{ .Type }}:\r\n" "exhaustedCount" = "🚨 Jumlah Habis {{ .Type }}:\r\n" "onlinesCount" = "🌐 Klien Online: {{ .Count }}\r\n" "disabled" = "🛑 Dinonaktifkan: {{ .Disabled }}\r\n" "depleteSoon" = "🔜 Habis Sebentar: {{ .Deplete }}\r\n\r\n" "backupTime" = "🗄 Waktu Backup: {{ .Time }}\r\n" "refreshedOn" = "\r\n📋🔄 Diperbarui Pada: {{ .Time }}\r\n\r\n" "yes" = "✅ Ya" "no" = "❌ Tidak" "received_id" = "🔑📥 ID diperbarui." "received_password" = "🔑📥 Kata sandi diperbarui." "received_email" = "📧📥 Email diperbarui." "received_comment" = "💬📥 Komentar diperbarui." "id_prompt" = "🔑 ID Default: {{ .ClientId }}\n\nMasukkan ID Anda." "pass_prompt" = "🔑 Kata Sandi Default: {{ .ClientPassword }}\n\nMasukkan kata sandi Anda." "email_prompt" = "📧 Email Default: {{ .ClientEmail }}\n\nMasukkan email Anda." "comment_prompt" = "💬 Komentar Default: {{ .ClientComment }}\n\nMasukkan komentar Anda." "inbound_client_data_id" = "🔄 Masuk: {{ .InboundRemark }}\n\n🔑 ID: {{ .ClientId }}\n📧 Email: {{ .ClientEmail }}\n📊 Lalu lintas: {{ .ClientTraffic }}\n📅 Tanggal Kedaluwarsa: {{ .ClientExp }}\n🌐 Batas IP: {{ .IpLimit }}\n💬 Komentar: {{ .ClientComment }}\n\nSekarang kamu bisa menambahkan klien ke inbound!" "inbound_client_data_pass" = "🔄 Masuk: {{ .InboundRemark }}\n\n🔑 Kata sandi: {{ .ClientPass }}\n📧 Email: {{ .ClientEmail }}\n📊 Lalu lintas: {{ .ClientTraffic }}\n📅 Tanggal Kedaluwarsa: {{ .ClientExp }}\n🌐 Batas IP: {{ .IpLimit }}\n💬 Komentar: {{ .ClientComment }}\n\nSekarang kamu bisa menambahkan klien ke inbound!" "cancel" = "❌ Proses Dibatalkan! \n\nAnda dapat /start lagi kapan saja. 🔄" "error_add_client" = "⚠️ Kesalahan:\n\n {{ .error }}" "using_default_value" = "Oke, saya akan tetap menggunakan nilai default. 😊" "incorrect_input" = "Masukan Anda tidak valid.\nFrasa harus berlanjut tanpa spasi.\nContoh benar: aaaaaa\nContoh salah: aaa aaa 🚫" "AreYouSure" = "Apakah kamu yakin? 🤔" "SuccessResetTraffic" = "📧 Email: {{ .ClientEmail }}\n🏁 Hasil: ✅ Berhasil" "FailedResetTraffic" = "📧 Email: {{ .ClientEmail }}\n🏁 Hasil: ❌ Gagal \n\n🛠️ Kesalahan: [ {{ .ErrorMessage }} ]" "FinishProcess" = "🔚 Proses reset traffic selesai untuk semua klien." [tgbot.buttons] "closeKeyboard" = "❌ Tutup Papan Ketik" "cancel" = "❌ Batal" "cancelReset" = "❌ Batal Reset" "cancelIpLimit" = "❌ Batal Batas IP" "confirmResetTraffic" = "✅ Konfirmasi Reset Lalu Lintas?" "confirmClearIps" = "✅ Konfirmasi Hapus IPs?" "confirmRemoveTGUser" = "✅ Konfirmasi Hapus Pengguna Telegram?" "confirmToggle" = "✅ Konfirmasi Aktifkan/Nonaktifkan Pengguna?" "dbBackup" = "Dapatkan Cadangan DB" "serverUsage" = "Penggunaan Server" "getInbounds" = "Dapatkan Inbounds" "depleteSoon" = "Habis Sebentar" "clientUsage" = "Dapatkan Penggunaan" "onlines" = "Klien Online" "commands" = "Perintah" "refresh" = "🔄 Perbarui" "clearIPs" = "❌ Hapus IPs" "removeTGUser" = "❌ Hapus Pengguna Telegram" "selectTGUser" = "👤 Pilih Pengguna Telegram" "selectOneTGUser" = "👤 Pilih Pengguna Telegram:" "resetTraffic" = "📈 Reset Lalu Lintas" "resetExpire" = "📅 Ubah Tanggal Kadaluarsa" "ipLog" = "🔢 Log IP" "ipLimit" = "🔢 Batas IP" "setTGUser" = "👤 Set Pengguna Telegram" "toggle" = "🔘 Aktifkan / Nonaktifkan" "custom" = "🔢 Kustom" "confirmNumber" = "✅ Konfirmasi: {{ .Num }}" "confirmNumberAdd" = "✅ Konfirmasi menambahkan: {{ .Num }}" "limitTraffic" = "🚧 Batas Lalu Lintas" "getBanLogs" = "Dapatkan Log Pemblokiran" "allClients" = "Semua Klien" "addClient" = "Tambah Klien" "submitDisable" = "Kirim Sebagai Nonaktif ☑️" "submitEnable" = "Kirim Sebagai Aktif ✅" "use_default" = "🏷️ Gunakan Default" "change_id" = "⚙️🔑 ID" "change_password" = "⚙️🔑 Kata Sandi" "change_email" = "⚙️📧 Email" "change_comment" = "⚙️💬 Komentar" "ResetAllTraffics" = "Reset Semua Lalu Lintas" "SortedTrafficUsageReport" = "Laporan Penggunaan Lalu Lintas yang Terurut" [tgbot.answers] "successfulOperation" = "✅ Operasi berhasil!" "errorOperation" = "❗ Kesalahan dalam operasi." "getInboundsFailed" = "❌ Gagal mendapatkan inbounds." "getClientsFailed" = "❌ Gagal mendapatkan klien." "canceled" = "❌ {{ .Email }}: Operasi dibatalkan." "clientRefreshSuccess" = "✅ {{ .Email }}: Klien diperbarui dengan berhasil." "IpRefreshSuccess" = "✅ {{ .Email }}: IP diperbarui dengan berhasil." "TGIdRefreshSuccess" = "✅ {{ .Email }}: Pengguna Telegram Klien diperbarui dengan berhasil." "resetTrafficSuccess" = "✅ {{ .Email }}: Lalu lintas direset dengan berhasil." "setTrafficLimitSuccess" = "✅ {{ .Email }}: Batas lalu lintas disimpan dengan berhasil." "expireResetSuccess" = "✅ {{ .Email }}: Hari kadaluarsa direset dengan berhasil." "resetIpSuccess" = "✅ {{ .Email }}: Batas IP {{ .Count }} disimpan dengan berhasil." "clearIpSuccess" = "✅ {{ .Email }}: IP dihapus dengan berhasil." "getIpLog" = "✅ {{ .Email }}: Dapatkan Log IP." "getUserInfo" = "✅ {{ .Email }}: Dapatkan Info Pengguna Telegram." "removedTGUserSuccess" = "✅ {{ .Email }}: Pengguna Telegram dihapus dengan berhasil." "enableSuccess" = "✅ {{ .Email }}: Diaktifkan dengan berhasil." "disableSuccess" = "✅ {{ .Email }}: Dinonaktifkan dengan berhasil." "askToAddUserId" = "Konfigurasi Anda tidak ditemukan!\r\nSilakan minta admin Anda untuk menggunakan ChatID Telegram Anda dalam konfigurasi Anda.\r\n\r\nChatID Pengguna Anda: {{ .TgUserID }}" "chooseClient" = "Pilih Klien untuk Inbound {{ .Inbound }}" "chooseInbound" = "Pilih Inbound" ================================================ FILE: web/translation/translate.ja_JP.toml ================================================ "username" = "ユーザー名" "password" = "パスワード" "login" = "ログイン" "confirm" = "確認" "cancel" = "キャンセル" "close" = "閉じる" "create" = "作成" "update" = "更新" "copy" = "コピー" "copied" = "コピー済み" "download" = "ダウンロード" "remark" = "備考" "enable" = "有効化" "protocol" = "プロトコル" "search" = "検索" "filter" = "フィルター" "loading" = "読み込み中..." "second" = "秒" "minute" = "分" "hour" = "時間" "day" = "日" "check" = "確認" "indefinite" = "無期限" "unlimited" = "無制限" "none" = "なし" "qrCode" = "QRコード" "info" = "詳細情報" "edit" = "編集" "delete" = "削除" "reset" = "リセット" "noData" = "データなし。" "copySuccess" = "コピー成功" "sure" = "確定" "encryption" = "暗号化" "useIPv4ForHost" = "ホストにIPv4を使用" "transmission" = "伝送" "host" = "ホスト" "path" = "パス" "camouflage" = "偽装" "status" = "ステータス" "enabled" = "有効" "disabled" = "無効" "depleted" = "消耗済み" "depletingSoon" = "間もなく消耗" "offline" = "オフライン" "online" = "オンライン" "domainName" = "ドメイン名" "monitor" = "監視" "certificate" = "証明書" "fail" = "失敗" "comment" = "コメント" "success" = "成功" "lastOnline" = "最終オンライン" "getVersion" = "バージョン取得" "install" = "インストール" "clients" = "クライアント" "usage" = "利用状況" "twoFactorCode" = "コード" "remained" = "残り" "security" = "セキュリティ" "secAlertTitle" = "セキュリティアラート" "secAlertSsl" = "この接続は安全ではありません。TLSを有効にしてデータ保護を行うまで、機密情報を入力しないでください。" "secAlertConf" = "一部の設定は脆弱です。潜在的な脆弱性を防ぐために、セキュリティプロトコルを強化することをお勧めします。" "secAlertSSL" = "セキュアな接続がありません。データ保護のためにTLS証明書をインストールしてください。" "secAlertPanelPort" = "デフォルトのポートにはセキュリティリスクがあります。ランダムなポートまたは特定のポートを設定してください。" "secAlertPanelURI" = "デフォルトのURIパスは安全ではありません。複雑なURIパスを設定してください。" "secAlertSubURI" = "サブスクリプションのデフォルトURIパスは安全ではありません。複雑なURIパスを設定してください。" "secAlertSubJsonURI" = "JSONサブスクリプションのデフォルトURIパスは安全ではありません。複雑なURIパスを設定してください。" "emptyDnsDesc" = "追加されたDNSサーバーはありません。" "emptyFakeDnsDesc" = "追加されたFake DNSサーバーはありません。" "emptyBalancersDesc" = "追加されたバランサーはありません。" "emptyReverseDesc" = "追加されたリバースプロキシはありません。" "somethingWentWrong" = "エラーが発生しました" [subscription] "title" = "サブスクリプション情報" "subId" = "サブスクリプションID" "status" = "ステータス" "downloaded" = "ダウンロード" "uploaded" = "アップロード" "expiry" = "有効期限" "totalQuota" = "合計クォータ" "individualLinks" = "個別リンク" "active" = "有効" "inactive" = "無効" "unlimited" = "無制限" "noExpiry" = "期限なし" [menu] "theme" = "テーマ" "dark" = "ダーク" "ultraDark" = "ウルトラダーク" "dashboard" = "ダッシュボード" "inbounds" = "インバウンド一覧" "settings" = "パネル設定" "xray" = "Xray設定" "logout" = "ログアウト" "link" = "リンク管理" [pages.login] "hello" = "こんにちは" "title" = "ようこそ" "loginAgain" = "ログインセッションが切れました。再度ログインしてください。" [pages.login.toasts] "invalidFormData" = "データ形式エラー" "emptyUsername" = "ユーザー名を入力してください" "emptyPassword" = "パスワードを入力してください" "wrongUsernameOrPassword" = "ユーザー名、パスワード、または二段階認証コードが無効です。" "successLogin" = "アカウントに正常にログインしました。" [pages.index] "title" = "システムステータス" "cpu" = "CPU" "logicalProcessors" = "論理プロセッサ" "frequency" = "周波数" "swap" = "スワップ" "storage" = "ストレージ" "memory" = "RAM" "threads" = "スレッド" "xrayStatus" = "Xray" "stopXray" = "停止" "restartXray" = "再起動" "xraySwitch" = "バージョン" "xraySwitchClick" = "切り替えるバージョンを選択してください" "xraySwitchClickDesk" = "慎重に選択してください。古いバージョンは現在の設定と互換性がない可能性があります。" "xrayStatusUnknown" = "不明" "xrayStatusRunning" = "実行中" "xrayStatusStop" = "停止" "xrayStatusError" = "エラー" "xrayErrorPopoverTitle" = "Xrayの実行中にエラーが発生しました" "operationHours" = "システム稼働時間" "systemLoad" = "システム負荷" "systemLoadDesc" = "過去1、5、15分間のシステム平均負荷" "connectionCount" = "接続数" "ipAddresses" = "IPアドレス" "toggleIpVisibility" = "IPの表示を切り替える" "overallSpeed" = "全体の速度" "upload" = "アップロード" "download" = "ダウンロード" "totalData" = "総データ量" "sent" = "送信" "received" = "受信" "documentation" = "ドキュメント" "xraySwitchVersionDialog" = "Xrayのバージョンを本当に変更しますか?" "xraySwitchVersionDialogDesc" = "Xrayのバージョンが#version#に変更されます。" "xraySwitchVersionPopover" = "Xrayの更新が成功しました" "geofileUpdateDialog" = "ジオファイルを本当に更新しますか?" "geofileUpdateDialogDesc" = "これにより#filename#ファイルが更新されます。" "geofilesUpdateDialogDesc" = "これにより、すべてのファイルが更新されます。" "geofilesUpdateAll" = "すべて更新" "geofileUpdatePopover" = "ジオファイルの更新が成功しました" "dontRefresh" = "インストール中、このページをリロードしないでください" "logs" = "ログ" "config" = "設定" "backup" = "バックアップ" "backupTitle" = "データベースのバックアップと復元" "exportDatabase" = "バックアップ" "exportDatabaseDesc" = "クリックして、現在のデータベースのバックアップを含む .db ファイルをデバイスにダウンロードします。" "importDatabase" = "復元" "importDatabaseDesc" = "クリックして、デバイスから .db ファイルを選択し、アップロードしてバックアップからデータベースを復元します。" "importDatabaseSuccess" = "データベースのインポートに成功しました" "importDatabaseError" = "データベースのインポート中にエラーが発生しました" "readDatabaseError" = "データベースの読み取り中にエラーが発生しました" "getDatabaseError" = "データベースの取得中にエラーが発生しました" "getConfigError" = "設定ファイルの取得中にエラーが発生しました" [pages.inbounds] "allTimeTraffic" = "総トラフィック" "allTimeTrafficUsage" = "これまでの総使用量" "title" = "インバウンド一覧" "totalDownUp" = "総アップロード / ダウンロード" "totalUsage" = "総使用量" "inboundCount" = "インバウンド数" "operate" = "メニュー" "enable" = "有効化" "remark" = "備考" "protocol" = "プロトコル" "port" = "ポート" "portMap" = "ポートマッピング" "traffic" = "トラフィック" "details" = "詳細情報" "transportConfig" = "トランスポート設定" "expireDate" = "有効期限" "createdAt" = "作成" "updatedAt" = "更新" "resetTraffic" = "トラフィックリセット" "addInbound" = "インバウンド追加" "generalActions" = "一般操作" "autoRefresh" = "自動更新" "autoRefreshInterval" = "間隔" "modifyInbound" = "インバウンド修正" "deleteInbound" = "インバウンド削除" "deleteInboundContent" = "インバウンドを削除してもよろしいですか?" "deleteClient" = "クライアント削除" "deleteClientContent" = "クライアントを削除してもよろしいですか?" "resetTrafficContent" = "トラフィックをリセットしてもよろしいですか?" "copyLink" = "リンクをコピー" "address" = "アドレス" "network" = "ネットワーク" "destinationPort" = "宛先ポート" "targetAddress" = "宛先アドレス" "monitorDesc" = "空白にするとすべてのIPを監視" "meansNoLimit" = "= 無制限(単位:GB)" "totalFlow" = "総トラフィック" "leaveBlankToNeverExpire" = "空白にすると期限なし" "noRecommendKeepDefault" = "デフォルト値を保持することをお勧めします" "certificatePath" = "ファイルパス" "certificateContent" = "ファイル内容" "publicKey" = "公開鍵" "privatekey" = "秘密鍵" "clickOnQRcode" = "QRコードをクリックしてコピー" "client" = "クライアント" "export" = "リンクエクスポート" "clone" = "複製" "cloneInbound" = "複製" "cloneInboundContent" = "このインバウンドルールは、ポート(Port)、リスニングIP(Listening IP)、クライアント(Clients)を除くすべての設定がクローンされます" "cloneInboundOk" = "クローン作成" "resetAllTraffic" = "すべてのインバウンドトラフィックをリセット" "resetAllTrafficTitle" = "すべてのインバウンドトラフィックをリセット" "resetAllTrafficContent" = "すべてのインバウンドトラフィックをリセットしてもよろしいですか?" "resetInboundClientTraffics" = "クライアントトラフィックをリセット" "resetInboundClientTrafficTitle" = "すべてのクライアントトラフィックをリセット" "resetInboundClientTrafficContent" = "このインバウンドクライアントのすべてのトラフィックをリセットしてもよろしいですか?" "resetAllClientTraffics" = "すべてのクライアントトラフィックをリセット" "resetAllClientTrafficTitle" = "すべてのクライアントトラフィックをリセット" "resetAllClientTrafficContent" = "すべてのクライアントのトラフィックをリセットしてもよろしいですか?" "delDepletedClients" = "トラフィックが尽きたクライアントを削除" "delDepletedClientsTitle" = "トラフィックが尽きたクライアントを削除" "delDepletedClientsContent" = "トラフィックが尽きたすべてのクライアントを削除してもよろしいですか?" "email" = "メールアドレス" "emailDesc" = "メールアドレスは一意でなければなりません" "IPLimit" = "IP制限" "IPLimitDesc" = "設定値を超えるとインバウンドトラフィックが無効になります。(0 = 無効)" "IPLimitlog" = "IPログ" "IPLimitlogDesc" = "IP履歴ログ(無効なインバウンドトラフィックを有効にするには、ログをクリアしてください)" "IPLimitlogclear" = "ログをクリア" "setDefaultCert" = "パネル設定から証明書を設定" "telegramDesc" = "TelegramチャットIDを提供してください。(ボットで'/id'コマンドを使用)または(@userinfobot)" "subscriptionDesc" = "サブスクリプションURLを見つけるには、“詳細情報”に移動してください。また、複数のクライアントに同じ名前を使用することができます。" "info" = "情報" "same" = "同じ" "inboundData" = "インバウンドデータ" "exportInbound" = "インバウンドルールをエクスポート" "import" = "インポート" "importInbound" = "インバウンドルールをインポート" "periodicTrafficResetTitle" = "トラフィックリセット" "periodicTrafficResetDesc" = "指定された間隔でトラフィックカウンタを自動的にリセット" "lastReset" = "最後のリセット" [pages.client] "add" = "クライアント追加" "edit" = "クライアント編集" "submitAdd" = "クライアント追加" "submitEdit" = "変更を保存" "clientCount" = "クライアント数" "bulk" = "一括作成" "method" = "方法" "first" = "最初" "last" = "最後" "prefix" = "プレフィックス" "postfix" = "サフィックス" "delayedStart" = "初回使用後に開始" "expireDays" = "期間" "days" = "日" "renew" = "自動更新" "renewDesc" = "期限が切れた後に自動更新。(0 = 無効)(単位:日)" [pages.inbounds.periodicTrafficReset] "never" = "なし" "daily" = "毎日" "weekly" = "毎週" "monthly" = "毎月" [pages.inbounds.toasts] "obtain" = "取得" "updateSuccess" = "更新が成功しました" "logCleanSuccess" = "ログがクリアされました" "inboundsUpdateSuccess" = "インバウンドが正常に更新されました" "inboundUpdateSuccess" = "インバウンドが正常に更新されました" "inboundCreateSuccess" = "インバウンドが正常に作成されました" "inboundDeleteSuccess" = "インバウンドが正常に削除されました" "inboundClientAddSuccess" = "インバウンドクライアントが追加されました" "inboundClientDeleteSuccess" = "インバウンドクライアントが削除されました" "inboundClientUpdateSuccess" = "インバウンドクライアントが更新されました" "delDepletedClientsSuccess" = "すべての枯渇したクライアントが削除されました" "resetAllClientTrafficSuccess" = "クライアントのすべてのトラフィックがリセットされました" "resetAllTrafficSuccess" = "すべてのトラフィックがリセットされました" "resetInboundClientTrafficSuccess" = "トラフィックがリセットされました" "trafficGetError" = "トラフィックの取得中にエラーが発生しました" "getNewX25519CertError" = "X25519証明書の取得中にエラーが発生しました。" "getNewmldsa65Error" = "mldsa65証明書の取得中にエラーが発生しました。" "getNewVlessEncError" = "VlessEnc証明書の取得中にエラーが発生しました。" [pages.inbounds.stream.general] "request" = "リクエスト" "response" = "レスポンス" "name" = "名前" "value" = "値" [pages.inbounds.stream.tcp] "version" = "バージョン" "method" = "方法" "path" = "パス" "status" = "ステータス" "statusDescription" = "ステータス説明" "requestHeader" = "リクエストヘッダー" "responseHeader" = "レスポンスヘッダー" [pages.settings] "title" = "パネル設定" "save" = "保存" "infoDesc" = "ここでのすべての変更は、保存してパネルを再起動する必要があります" "restartPanel" = "パネル再起動" "restartPanelDesc" = "パネルを再起動してもよろしいですか?再起動後にパネルにアクセスできない場合は、サーバーでパネルログを確認してください" "restartPanelSuccess" = "パネルの再起動に成功しました" "actions" = "操作" "resetDefaultConfig" = "デフォルト設定にリセット" "panelSettings" = "一般" "securitySettings" = "セキュリティ設定" "TGBotSettings" = "Telegramボット設定" "panelListeningIP" = "パネル監視IP" "panelListeningIPDesc" = "デフォルトではすべてのIPを監視する" "panelListeningDomain" = "パネル監視ドメイン" "panelListeningDomainDesc" = "デフォルトで空白の場合、すべてのドメインとIPアドレスを監視する" "panelPort" = "パネル監視ポート" "panelPortDesc" = "再起動で有効" "publicKeyPath" = "パネル証明書公開鍵ファイルパス" "publicKeyPathDesc" = "'/'で始まる絶対パスを入力" "privateKeyPath" = "パネル証明書秘密鍵ファイルパス" "privateKeyPathDesc" = "'/'で始まる絶対パスを入力" "panelUrlPath" = "パネルURLルートパス" "panelUrlPathDesc" = "'/'で始まり、'/'で終わる必要があります" "pageSize" = "ページサイズ" "pageSizeDesc" = "インバウンドテーブルのページサイズを定義します。0を設定すると無効化されます" "remarkModel" = "備考モデルと区切り記号" "datepicker" = "日付ピッカー" "datepickerPlaceholder" = "日付を選択" "datepickerDescription" = "日付選択カレンダーで有効期限を指定する" "sampleRemark" = "備考の例" "oldUsername" = "旧ユーザー名" "currentPassword" = "旧パスワード" "newUsername" = "新しいユーザー名" "newPassword" = "新しいパスワード" "telegramBotEnable" = "Telegramボットを有効にする" "telegramBotEnableDesc" = "Telegramボット機能を有効にする" "telegramToken" = "Telegramボットトークン" "telegramTokenDesc" = "'@BotFather'から取得したTelegramボットトークン" "telegramProxy" = "SOCKS5プロキシ" "telegramProxyDesc" = "SOCKS5プロキシを有効にしてTelegramに接続する(ガイドに従って設定を調整)" "telegramAPIServer" = "Telegram APIサーバー" "telegramAPIServerDesc" = "使用するTelegram APIサーバー。空白の場合はデフォルトサーバーを使用する" "telegramChatId" = "管理者チャットID" "telegramChatIdDesc" = "Telegram管理者チャットID(複数の場合はカンマで区切る)@userinfobotで取得するか、ボットで'/id'コマンドを使用して取得する" "telegramNotifyTime" = "通知時間" "telegramNotifyTimeDesc" = "定期的なTelegramボット通知時間を設定する(crontab時間形式を使用)" "tgNotifyBackup" = "データベースバックアップ" "tgNotifyBackupDesc" = "レポート付きのデータベースバックアップファイルを送信" "tgNotifyLogin" = "ログイン通知" "tgNotifyLoginDesc" = "誰かがパネルにログインしようとしたときに、ユーザー名、IPアドレス、時間を表示する" "sessionMaxAge" = "セッション期間" "sessionMaxAgeDesc" = "ログイン状態を保持する期間(単位:分)" "expireTimeDiff" = "有効期限通知のしきい値" "expireTimeDiffDesc" = "このしきい値に達した場合、有効期限に関する通知を受け取る(単位:日)" "trafficDiff" = "トラフィック消耗しきい値" "trafficDiffDesc" = "このしきい値に達した場合、トラフィック消耗に関する通知を受け取る(単位:GB)" "tgNotifyCpu" = "CPU負荷通知しきい値" "tgNotifyCpuDesc" = "CPU負荷がこのしきい値を超えた場合、通知を受け取る(単位:%)" "timeZone" = "タイムゾーン" "timeZoneDesc" = "定時タスクはこのタイムゾーンの時間に従って実行される" "subSettings" = "サブスクリプション設定" "subEnable" = "サブスクリプションサービスを有効にする" "subEnableDesc" = "サブスクリプションサービス機能を有効にする" "subJsonEnable" = "JSON サブスクリプションのエンドポイントを個別に有効/無効にする。" "subTitle" = "サブスクリプションタイトル" "subTitleDesc" = "VPNクライアントに表示されるタイトル" "subSupportUrl" = "サポートURL" "subSupportUrlDesc" = "VPNクライアントに表示されるテクニカルサポートへのリンク" "subProfileUrl" = "プロフィールURL" "subProfileUrlDesc" = "VPNクライアントに表示されるWebサイトへのリンク" "subAnnounce" = "お知らせ" "subAnnounceDesc" = "VPNクライアントに表示されるお知らせのテキスト" "subEnableRouting" = "ルーティングを有効化" "subEnableRoutingDesc" = "VPNクライアントでルーティングを有効にするためのグローバル設定。(Happのみ)" "subRoutingRules" = "ルーティングルール" "subRoutingRulesDesc" = "VPNクライアントのグローバルルーティングルール。(Happのみ)" "subListen" = "監視IP" "subListenDesc" = "サブスクリプションサービスが監視するIPアドレス(空白にするとすべてのIPを監視)" "subPort" = "監視ポート" "subPortDesc" = "サブスクリプションサービスが監視するポート番号(使用されていないポートである必要があります)" "subCertPath" = "公開鍵パス" "subCertPathDesc" = "サブスクリプションサービスで使用する公開鍵ファイルのパス('/'で始まる)" "subKeyPath" = "秘密鍵パス" "subKeyPathDesc" = "サブスクリプションサービスで使用する秘密鍵ファイルのパス('/'で始まる)" "subPath" = "URIパス" "subPathDesc" = "サブスクリプションサービスで使用するURIパス('/'で始まり、'/'で終わる)" "subDomain" = "監視ドメイン" "subDomainDesc" = "サブスクリプションサービスが監視するドメイン(空白にするとすべてのドメインとIPを監視)" "subUpdates" = "更新間隔" "subUpdatesDesc" = "クライアントアプリケーションでサブスクリプションURLの更新間隔(単位:時間)" "subEncrypt" = "エンコード" "subEncryptDesc" = "サブスクリプションサービスが返す内容をBase64エンコードする" "subShowInfo" = "利用情報を表示" "subShowInfoDesc" = "クライアントアプリで残りのトラフィックと日付情報を表示する" "subURI" = "リバースプロキシURI" "subURIDesc" = "プロキシ後ろのサブスクリプションURLのURIパスに使用する" "externalTrafficInformEnable" = "外部トラフィック情報" "externalTrafficInformEnableDesc" = "トラフィックの更新ごとに外部 API に通知します。" "externalTrafficInformURI" = "外部トラフィック通知 URI" "externalTrafficInformURIDesc" = "トラフィックの更新ごとに外部 API に通知します。" "fragment" = "フラグメント" "fragmentDesc" = "TLS helloパケットのフラグメントを有効にする" "fragmentSett" = "設定" "noisesDesc" = "Noisesを有効にする" "noisesSett" = "Noises設定" "mux" = "マルチプレクサ" "muxDesc" = "確立されたストリーム内で複数の独立したストリームを伝送する" "muxSett" = "マルチプレクサ設定" "direct" = "直接接続" "directDesc" = "特定の国のドメインまたはIP範囲に直接接続する" "notifications" = "通知" "certs" = "証明書" "externalTraffic" = "外部トラフィック" "dateAndTime" = "日付と時刻" "proxyAndServer" = "プロキシとサーバー" "intervals" = "間隔" "information" = "情報" "language" = "言語" "telegramBotLanguage" = "Telegram Botの言語" [pages.xray] "title" = "Xray 設定" "save" = "保存" "restart" = "Xray 再起動" "restartSuccess" = "Xrayの再起動に成功しました" "stopSuccess" = "Xrayが正常に停止しました" "restartError" = "Xrayの再起動中にエラーが発生しました。" "stopError" = "Xrayの停止中にエラーが発生しました。" "basicTemplate" = "基本設定" "advancedTemplate" = "高度な設定" "generalConfigs" = "一般設定" "generalConfigsDesc" = "これらのオプションは一般設定を決定します" "logConfigs" = "ログ" "logConfigsDesc" = "ログはサーバーのパフォーマンスに影響を与える可能性があるため、必要な場合にのみ有効にすることをお勧めします" "blockConfigsDesc" = "これらのオプションは、特定のプロトコルやウェブサイトへのユーザー接続をブロックします" "basicRouting" = "基本ルーティング" "blockConnectionsConfigsDesc" = "これらのオプションにより、特定のリクエスト元の国に基づいてトラフィックをブロックします。" "directConnectionsConfigsDesc" = "直接接続により、特定のトラフィックが他のサーバーを経由しないようにします。" "blockips" = "IPをブロック" "blockdomains" = "ドメインをブロック" "directips" = "直接IP" "directdomains" = "直接ドメイン" "ipv4Routing" = "IPv4 ルーティング" "ipv4RoutingDesc" = "このオプションはIPv4のみを介してターゲットドメインへルーティングします" "warpRouting" = "WARP ルーティング" "warpRoutingDesc" = "注意:これらのオプションを使用する前に、パネルのGitHubの手順に従って、サーバーにsocks5プロキシモードでWARPをインストールしてください。WARPはCloudflareサーバー経由でトラフィックをウェブサイトにルーティングします。" "Template" = "高度なXray設定テンプレート" "TemplateDesc" = "最終的なXray設定ファイルはこのテンプレートに基づいて生成されます" "FreedomStrategy" = "Freedom プロトコル戦略" "FreedomStrategyDesc" = "Freedomプロトコル内のネットワークの出力戦略を設定する" "RoutingStrategy" = "ルーティングドメイン戦略設定" "RoutingStrategyDesc" = "DNS解決の全体的なルーティング戦略を設定する" "outboundTestUrl" = "アウトバウンドテスト URL" "outboundTestUrlDesc" = "アウトバウンド接続テストに使用する URL。既定値" "Torrent" = "BitTorrent プロトコルをブロック" "Inbounds" = "インバウンドルール" "InboundsDesc" = "特定のクライアントからのトラフィックを受け入れる" "Outbounds" = "アウトバウンドルール" "Balancers" = "負荷分散" "OutboundsDesc" = "アウトバウンドトラフィックの送信方法を設定する" "Routings" = "ルーティングルール" "RoutingsDesc" = "各ルールの優先順位が重要です" "completeTemplate" = "すべて" "logLevel" = "ログレベル" "logLevelDesc" = "エラーログのレベルを指定し、記録する情報を示します" "accessLog" = "アクセスログ" "accessLogDesc" = "アクセスログのファイルパス。特殊値 'none' はアクセスログを無効にします" "errorLog" = "エラーログ" "errorLogDesc" = "エラーログのファイルパス。特殊値 'none' はエラーログを無効にします" "dnsLog" = "DNS ログ" "dnsLogDesc" = "DNSクエリのログを有効にするかどうか" "maskAddress" = "アドレスをマスク" "maskAddressDesc" = "IPアドレスをマスクし、有効にするとログに表示されるIPアドレスを自動的に置き換えます" "statistics" = "統計" "statsInboundUplink" = "インバウンドアップロード統計" "statsInboundUplinkDesc" = "すべてのインバウンドプロキシのアップストリームトラフィックの統計収集を有効にします。" "statsInboundDownlink" = "インバウンドダウンロード統計" "statsInboundDownlinkDesc" = "すべてのインバウンドプロキシのダウンストリームトラフィックの統計収集を有効にします。" "statsOutboundUplink" = "アウトバウンドアップロード統計" "statsOutboundUplinkDesc" = "すべてのアウトバウンドプロキシのアップストリームトラフィックの統計収集を有効にします。" "statsOutboundDownlink" = "アウトバウンドダウンロード統計" "statsOutboundDownlinkDesc" = "すべてのアウトバウンドプロキシのダウンストリームトラフィックの統計収集を有効にします。" [pages.xray.rules] "first" = "最初" "last" = "最後" "up" = "上へ" "down" = "下へ" "source" = "ソース" "dest" = "宛先アドレス" "inbound" = "インバウンド" "outbound" = "アウトバウンド" "balancer" = "負荷分散" "info" = "情報" "add" = "ルール追加" "edit" = "ルール編集" "useComma" = "カンマ区切りの項目" [pages.xray.outbound] "addOutbound" = "アウトバウンド追加" "addReverse" = "リバース追加" "editOutbound" = "アウトバウンド編集" "editReverse" = "リバース編集" "tag" = "タグ" "tagDesc" = "一意のタグ" "address" = "アドレス" "reverse" = "リバース" "domain" = "ドメイン" "type" = "タイプ" "bridge" = "ブリッジ" "portal" = "ポータル" "link" = "リンク" "intercon" = "インターコネクション" "settings" = "設定" "accountInfo" = "アカウント情報" "outboundStatus" = "アウトバウンドステータス" "sendThrough" = "送信経路" "test" = "テスト" "testResult" = "テスト結果" "testing" = "接続をテスト中..." "testSuccess" = "テスト成功" "testFailed" = "テスト失敗" "testError" = "アウトバウンドのテストに失敗しました" [pages.xray.balancer] "addBalancer" = "負荷分散追加" "editBalancer" = "負荷分散編集" "balancerStrategy" = "戦略" "balancerSelectors" = "セレクター" "tag" = "タグ" "tagDesc" = "一意のタグ" "balancerDesc" = "balancerTagとoutboundTagは同時に使用できません。同時に使用された場合、outboundTagのみが有効になります。" [pages.xray.wireguard] "secretKey" = "シークレットキー" "publicKey" = "公開鍵" "allowedIPs" = "許可されたIP" "endpoint" = "エンドポイント" "psk" = "共有キー" "domainStrategy" = "ドメイン戦略" [pages.xray.tun] "nameDesc" = "TUN インターフェースの名前。デフォルトは 'xray0' です" "mtuDesc" = "最大伝送単位。データパケットの最大サイズ。デフォルトは 1500 です" "userLevel" = "ユーザーレベル" "userLevelDesc" = "このインバウンドを通じて確立されたすべての接続は、このユーザーレベルを使用します。デフォルトは 0 です" [pages.xray.dns] "enable" = "DNSを有効にする" "enableDesc" = "組み込みDNSサーバーを有効にする" "tag" = "DNSインバウンドタグ" "tagDesc" = "このタグはルーティングルールでインバウンドタグとして使用できます" "clientIp" = "クライアントIP" "clientIpDesc" = "DNSクエリ中に指定されたIPの位置をサーバーに通知するために使用されます" "disableCache" = "キャッシュを無効にする" "disableCacheDesc" = "DNSキャッシュを無効にします" "disableFallback" = "フォールバックを無効にする" "disableFallbackDesc" = "フォールバックDNSクエリを無効にします" "disableFallbackIfMatch" = "一致した場合にフォールバックを無効にする" "disableFallbackIfMatchDesc" = "DNSサーバーの一致するドメインリストにヒットした場合、フォールバックDNSクエリを無効にします" "enableParallelQuery" = "並列クエリを有効にする" "enableParallelQueryDesc" = "複数のサーバーへの並列DNSクエリを有効にして、より高速な解決を実現" "strategy" = "クエリ戦略" "strategyDesc" = "ドメイン名解決の全体的な戦略" "add" = "サーバー追加" "edit" = "サーバー編集" "domains" = "ドメイン" "expectIPs" = "期待されるIP" "unexpectIPs" = "予期しないIP" "useSystemHosts" = "システムのHostsを使用" "useSystemHostsDesc" = "インストール済みシステムのhostsファイルを使用する" "usePreset" = "テンプレートを使用" "dnsPresetTitle" = "DNSテンプレート" "dnsPresetFamily" = "ファミリー" [pages.xray.fakedns] "add" = "フェイクDNS追加" "edit" = "フェイクDNS編集" "ipPool" = "IPプールサブネット" "poolSize" = "プールサイズ" [pages.settings.security] "admin" = "管理者の資格情報" "twoFactor" = "二段階認証" "twoFactorEnable" = "2FAを有効化" "twoFactorEnableDesc" = "セキュリティを強化するために追加の認証層を追加します。" "twoFactorModalSetTitle" = "二段階認証を有効にする" "twoFactorModalDeleteTitle" = "二段階認証を無効にする" "twoFactorModalSteps" = "二段階認証を設定するには、次の手順を実行してください:" "twoFactorModalFirstStep" = "1. 認証アプリでこのQRコードをスキャンするか、QRコード近くのトークンをコピーしてアプリに貼り付けます" "twoFactorModalSecondStep" = "2. アプリからコードを入力してください" "twoFactorModalRemoveStep" = "二段階認証を削除するには、アプリからコードを入力してください。" "twoFactorModalChangeCredentialsTitle" = "認証情報の変更" "twoFactorModalChangeCredentialsStep" = "管理者の認証情報を変更するには、アプリケーションからコードを入力してください。" "twoFactorModalSetSuccess" = "二要素認証が正常に設定されました" "twoFactorModalDeleteSuccess" = "二要素認証が正常に削除されました" "twoFactorModalError" = "コードが間違っています" [pages.settings.toasts] "modifySettings" = "パラメーターが変更されました。" "getSettings" = "パラメーターの取得中にエラーが発生しました" "modifyUserError" = "管理者認証情報の変更中にエラーが発生しました。" "modifyUser" = "管理者の認証情報を正常に変更しました。" "originalUserPassIncorrect" = "旧ユーザー名または旧パスワードが間違っています" "userPassMustBeNotEmpty" = "新しいユーザー名と新しいパスワードは空にできません" "getOutboundTrafficError" = "送信トラフィックの取得エラー" "resetOutboundTrafficError" = "送信トラフィックのリセットエラー" [tgbot] "keyboardClosed" = "❌ キーボードを閉じました!" "noResult" = "❗ 結果がありません!" "noQuery" = "❌ クエリが見つかりません!コマンドを再利用してください!" "wentWrong" = "❌ 何かがうまくいかなかった!" "noIpRecord" = "❗ IPレコードがありません!" "noInbounds" = "❗ インバウンドが見つかりません!" "unlimited" = "♾ 無制限(リセット)" "add" = "追加" "month" = "月" "months" = "ヶ月" "day" = "日" "days" = "日間" "hours" = "時間" "minutes" = "分" "unknown" = "不明" "inbounds" = "インバウンド" "clients" = "クライアント" "offline" = "🔴 オフライン" "online" = "🟢 オンライン" [tgbot.commands] "unknown" = "❗ 不明なコマンド" "pleaseChoose" = "👇 選択してください:\r\n" "help" = "🤖 このボットをご利用いただきありがとうございます!サーバーから特定のデータを提供し、必要な変更を行うことができます。\r\n\r\n" "start" = "👋 こんにちは、{{ .Firstname }}。\r\n" "welcome" = "🤖 {{ .Hostname }} 管理ボットへようこそ。\r\n" "status" = "✅ ボットは正常に動作しています!" "usage" = "❗ 検索するテキストを入力してください!" "getID" = "🆔 あなたのIDは:{{ .ID }}" "helpAdminCommands" = "Xray Coreを再起動するには:\r\n/restart\r\n\r\nクライアントの電子メールを検索するには:\r\n/usage [電子メール]\r\n\r\nインバウンド(クライアントの統計情報を含む)を検索するには:\r\n/inbound [備考]\r\n\r\nTelegramチャットID:\r\n/id" "helpClientCommands" = "統計情報を検索するには、次のコマンドを使用してください:\r\n/usage [電子メール]\r\n\r\nTelegramチャットID:\r\n/id" "restartUsage" = "\r\n\r\n/restart" "restartSuccess" = "✅ 操作成功!" "restartFailed" = "❗ 操作エラー。\r\n\r\nエラー: {{ .Error }}" "xrayNotRunning" = "❗ Xray Core は動作していません。" "startDesc" = "メインメニューを表示" "helpDesc" = "ボットのヘルプ" "statusDesc" = "ボットの状態を確認" "idDesc" = "Telegram IDを表示" [tgbot.messages] "cpuThreshold" = "🔴 CPU使用率は{{ .Percent }}%、しきい値{{ .Threshold }}%を超えました" "selectUserFailed" = "❌ ユーザーの選択に失敗しました!" "userSaved" = "✅ Telegramユーザーが保存されました。" "loginSuccess" = "✅ パネルに正常にログインしました。\r\n" "loginFailed" = "❗️ パネルのログインに失敗しました。\r\n" "2faFailed" = "2FAエラー" "report" = "🕰 定期報告:{{ .RunTime }}\r\n" "datetime" = "⏰ 日時:{{ .DateTime }}\r\n" "hostname" = "💻 ホスト名:{{ .Hostname }}\r\n" "version" = "🚀 X-UI バージョン:{{ .Version }}\r\n" "xrayVersion" = "📡 Xray バージョン: {{ .XrayVersion }}\r\n" "ipv6" = "🌐 IPv6:{{ .IPv6 }}\r\n" "ipv4" = "🌐 IPv4:{{ .IPv4 }}\r\n" "ip" = "🌐 IP:{{ .IP }}\r\n" "ips" = "🔢 IPアドレス:\r\n{{ .IPs }}\r\n" "serverUpTime" = "⏳ サーバー稼働時間:{{ .UpTime }} {{ .Unit }}\r\n" "serverLoad" = "📈 サーバー負荷:{{ .Load1 }}, {{ .Load2 }}, {{ .Load3 }}\r\n" "serverMemory" = "📋 サーバーメモリ:{{ .Current }}/{{ .Total }}\r\n" "tcpCount" = "🔹 TCP接続数:{{ .Count }}\r\n" "udpCount" = "🔸 UDP接続数:{{ .Count }}\r\n" "traffic" = "🚦 トラフィック:{{ .Total }} (↑{{ .Upload }},↓{{ .Download }})\r\n" "xrayStatus" = "ℹ️ Xrayステータス:{{ .State }}\r\n" "username" = "👤 ユーザー名:{{ .Username }}\r\n" "password" = "👤 パスワード: {{ .Password }}\r\n" "time" = "⏰ 時間:{{ .Time }}\r\n" "inbound" = "📍 インバウンド:{{ .Remark }}\r\n" "port" = "🔌 ポート:{{ .Port }}\r\n" "expire" = "📅 有効期限:{{ .Time }}\r\n" "expireIn" = "📅 残り時間:{{ .Time }}\r\n" "active" = "💡 有効:{{ .Enable }}\r\n" "enabled" = "🚨 有効化済み:{{ .Enable }}\r\n" "online" = "🌐 接続ステータス:{{ .Status }}\r\n" "lastOnline" = "🔙 最終オンライン: {{ .Time }}\r\n" "email" = "📧 メール:{{ .Email }}\r\n" "upload" = "🔼 アップロード↑:{{ .Upload }}\r\n" "download" = "🔽 ダウンロード↓:{{ .Download }}\r\n" "total" = "📊 合計:{{ .UpDown }} / {{ .Total }}\r\n" "TGUser" = "👤 Telegramユーザー:{{ .TelegramID }}\r\n" "exhaustedMsg" = "🚨 消耗済みの {{ .Type }}:\r\n" "exhaustedCount" = "🚨 消耗済みの {{ .Type }} 数量:\r\n" "onlinesCount" = "🌐 オンラインクライアント:{{ .Count }}\r\n" "disabled" = "🛑 無効化:{{ .Disabled }}\r\n" "depleteSoon" = "🔜 間もなく消耗:{{ .Deplete }}\r\n\r\n" "backupTime" = "🗄 バックアップ時間:{{ .Time }}\r\n" "refreshedOn" = "\r\n📋🔄 更新時間:{{ .Time }}\r\n\r\n" "yes" = "✅ はい" "no" = "❌ いいえ" "received_id" = "🔑📥 IDが更新されました。" "received_password" = "🔑📥 パスワードが更新されました。" "received_email" = "📧📥 メールが更新されました。" "received_comment" = "💬📥 コメントが更新されました。" "id_prompt" = "🔑 デフォルトID: {{ .ClientId }}\n\nIDを入力してください。" "pass_prompt" = "🔑 デフォルトパスワード: {{ .ClientPassword }}\n\nパスワードを入力してください。" "email_prompt" = "📧 デフォルトメール: {{ .ClientEmail }}\n\nメールを入力してください。" "comment_prompt" = "💬 デフォルトコメント: {{ .ClientComment }}\n\nコメントを入力してください。" "inbound_client_data_id" = "🔄 インバウンド: {{ .InboundRemark }}\n\n🔑 ID: {{ .ClientId }}\n📧 メール: {{ .ClientEmail }}\n📊 トラフィック: {{ .ClientTraffic }}\n📅 有効期限: {{ .ClientExp }}\n🌐 IP制限: {{ .IpLimit }}\n💬 コメント: {{ .ClientComment }}\n\n今すぐこのクライアントをインバウンドに追加できます!" "inbound_client_data_pass" = "🔄 インバウンド: {{ .InboundRemark }}\n\n🔑 パスワード: {{ .ClientPass }}\n📧 メール: {{ .ClientEmail }}\n📊 トラフィック: {{ .ClientTraffic }}\n📅 有効期限: {{ .ClientExp }}\n🌐 IP制限: {{ .IpLimit }}\n💬 コメント: {{ .ClientComment }}\n\n今すぐこのクライアントをインバウンドに追加できます!" "cancel" = "❌ プロセスがキャンセルされました!\n\nいつでも /start で再開できます。 🔄" "error_add_client" = "⚠️ エラー:\n\n {{ .error }}" "using_default_value" = "わかりました、デフォルト値を使用します。 😊" "incorrect_input" = "入力が無効です。\nフレーズはスペースなしで続けて入力してください。\n正しい例: aaaaaa\n間違った例: aaa aaa 🚫" "AreYouSure" = "本当にいいですか?🤔" "SuccessResetTraffic" = "📧 メール: {{ .ClientEmail }}\n🏁 結果: ✅ 成功" "FailedResetTraffic" = "📧 メール: {{ .ClientEmail }}\n🏁 結果: ❌ 失敗 \n\n🛠️ エラー: [ {{ .ErrorMessage }} ]" "FinishProcess" = "🔚 すべてのクライアントのトラフィックリセットが完了しました。" [tgbot.buttons] "closeKeyboard" = "❌ キーボードを閉じる" "cancel" = "❌ キャンセル" "cancelReset" = "❌ リセットをキャンセル" "cancelIpLimit" = "❌ IP制限をキャンセル" "confirmResetTraffic" = "✅ トラフィックをリセットしますか?" "confirmClearIps" = "✅ IPをクリアしますか?" "confirmRemoveTGUser" = "✅ Telegramユーザーを削除しますか?" "confirmToggle" = "✅ ユーザーを有効/無効にしますか?" "dbBackup" = "データベースバックアップを取得" "serverUsage" = "サーバーの使用状況" "getInbounds" = "インバウンド情報を取得" "depleteSoon" = "間もなく消耗" "clientUsage" = "使用状況を取得" "onlines" = "オンラインクライアント" "commands" = "コマンド" "refresh" = "🔄 更新" "clearIPs" = "❌ IPをクリア" "removeTGUser" = "❌ Telegramユーザーを削除" "selectTGUser" = "👤 Telegramユーザーを選択" "selectOneTGUser" = "👤 1人のTelegramユーザーを選択:" "resetTraffic" = "📈 トラフィックをリセット" "resetExpire" = "📅 有効期限を変更" "ipLog" = "🔢 IPログ" "ipLimit" = "🔢 IP制限" "setTGUser" = "👤 Telegramユーザーを設定" "toggle" = "🔘 有効/無効" "custom" = "🔢 カスタム" "confirmNumber" = "✅ 確認: {{ .Num }}" "confirmNumberAdd" = "✅ 追加を確認:{{ .Num }}" "limitTraffic" = "🚧 トラフィック制限" "getBanLogs" = "禁止ログ" "allClients" = "すべてのクライアント" "addClient" = "クライアントを追加" "submitDisable" = "無効として送信 ☑️" "submitEnable" = "有効として送信 ✅" "use_default" = "🏷️ デフォルトを使用" "change_id" = "⚙️🔑 ID" "change_password" = "⚙️🔑 パスワード" "change_email" = "⚙️📧 メールアドレス" "change_comment" = "⚙️💬 コメント" "ResetAllTraffics" = "すべてのトラフィックをリセット" "SortedTrafficUsageReport" = "ソートされたトラフィック使用レポート" [tgbot.answers] "successfulOperation" = "✅ 成功!" "errorOperation" = "❗ 操作エラー。" "getInboundsFailed" = "❌ インバウンド情報の取得に失敗しました。" "getClientsFailed" = "❌ クライアントの取得に失敗しました。" "canceled" = "❌ {{ .Email }}:操作がキャンセルされました。" "clientRefreshSuccess" = "✅ {{ .Email }}:クライアントが正常に更新されました。" "IpRefreshSuccess" = "✅ {{ .Email }}:IPが正常に更新されました。" "TGIdRefreshSuccess" = "✅ {{ .Email }}:クライアントのTelegramユーザーが正常に更新されました。" "resetTrafficSuccess" = "✅ {{ .Email }}:トラフィックが正常にリセットされました。" "setTrafficLimitSuccess" = "✅ {{ .Email }}:トラフィック制限が正常に保存されました。" "expireResetSuccess" = "✅ {{ .Email }}:有効期限の日数が正常にリセットされました。" "resetIpSuccess" = "✅ {{ .Email }}:IP制限数が正常に保存されました:{{ .Count }}。" "clearIpSuccess" = "✅ {{ .Email }}:IPが正常にクリアされました。" "getIpLog" = "✅ {{ .Email }}:IPログの取得。" "getUserInfo" = "✅ {{ .Email }}:Telegramユーザー情報の取得。" "removedTGUserSuccess" = "✅ {{ .Email }}:Telegramユーザーが正常に削除されました。" "enableSuccess" = "✅ {{ .Email }}:正常に有効化されました。" "disableSuccess" = "✅ {{ .Email }}:正常に無効化されました。" "askToAddUserId" = "設定が見つかりませんでした!\r\n管理者に問い合わせて、設定にTelegramユーザーのChatIDを使用してください。\r\n\r\nあなたのユーザーChatID:{{ .TgUserID }}" "chooseClient" = "インバウンド {{ .Inbound }} のクライアントを選択" "chooseInbound" = "インバウンドを選択" ================================================ FILE: web/translation/translate.pt_BR.toml ================================================ "username" = "Nome de Usuário" "password" = "Senha" "login" = "Entrar" "confirm" = "Confirmar" "cancel" = "Cancelar" "close" = "Fechar" "create" = "Criar" "update" = "Atualizar" "copy" = "Copiar" "copied" = "Copiado" "download" = "Baixar" "remark" = "Observação" "enable" = "Ativado" "protocol" = "Protocolo" "search" = "Pesquisar" "filter" = "Filtrar" "loading" = "Carregando..." "second" = "Segundo" "minute" = "Minuto" "hour" = "Hora" "day" = "Dia" "check" = "Verificar" "indefinite" = "Indeterminado" "unlimited" = "Ilimitado" "none" = "Nada" "qrCode" = "Código QR" "info" = "Mais Informações" "edit" = "Editar" "delete" = "Excluir" "reset" = "Redefinir" "noData" = "Sem dados." "copySuccess" = "Copiado com Sucesso" "sure" = "Certo" "encryption" = "Criptografia" "useIPv4ForHost" = "Usar IPv4 para o host" "transmission" = "Transmissão" "host" = "Servidor" "path" = "Caminho" "camouflage" = "Ofuscação" "status" = "Status" "enabled" = "Ativado" "disabled" = "Desativado" "depleted" = "Encerrado" "depletingSoon" = "Esgotando" "offline" = "Offline" "online" = "Online" "domainName" = "Nome de Domínio" "monitor" = "IP de Escuta" "certificate" = "Certificado Digital" "fail" = "Falhou" "comment" = "Comentário" "success" = "Com Sucesso" "lastOnline" = "Última vez online" "getVersion" = "Obter Versão" "install" = "Instalar" "clients" = "Clientes" "usage" = "Uso" "twoFactorCode" = "Código" "remained" = "Restante" "security" = "Segurança" "secAlertTitle" = "Alerta de Segurança" "secAlertSsl" = "Esta conexão não é segura. Evite inserir informações confidenciais até que o TLS seja ativado para proteção de dados." "secAlertConf" = "Algumas configurações estão vulneráveis a ataques. Recomenda-se reforçar os protocolos de segurança para evitar possíveis violações." "secAlertSSL" = "O painel não possui uma conexão segura. Instale o certificado TLS para proteção de dados." "secAlertPanelPort" = "A porta padrão do painel é vulnerável. Configure uma porta aleatória ou específica." "secAlertPanelURI" = "O caminho URI padrão do painel não é seguro. Configure um caminho URI complexo." "secAlertSubURI" = "O caminho URI padrão de inscrição não é seguro. Configure um caminho URI complexo." "secAlertSubJsonURI" = "O caminho URI JSON de inscrição padrão não é seguro. Configure um caminho URI complexo." "emptyDnsDesc" = "Nenhum servidor DNS adicionado." "emptyFakeDnsDesc" = "Nenhum servidor Fake DNS adicionado." "emptyBalancersDesc" = "Nenhum balanceador adicionado." "emptyReverseDesc" = "Nenhum proxy reverso adicionado." "somethingWentWrong" = "Algo deu errado" [subscription] "title" = "Informações da assinatura" "subId" = "ID da assinatura" "status" = "Status" "downloaded" = "Baixado" "uploaded" = "Enviado" "expiry" = "Validade" "totalQuota" = "Cota total" "individualLinks" = "Links individuais" "active" = "Ativo" "inactive" = "Inativo" "unlimited" = "Ilimitado" "noExpiry" = "Sem validade" [menu] "theme" = "Tema" "dark" = "Escuro" "ultraDark" = "Ultra Escuro" "dashboard" = "Visão Geral" "inbounds" = "Inbounds" "settings" = "Panel Settings" "xray" = "Xray Configs" "logout" = "Sair" "link" = "Gerenciar" [pages.login] "hello" = "Olá" "title" = "Bem-vindo" "loginAgain" = "Sua sessão expirou, faça login novamente" [pages.login.toasts] "invalidFormData" = "O formato dos dados de entrada é inválido." "emptyUsername" = "Nome de usuário é obrigatório" "emptyPassword" = "Senha é obrigatória" "wrongUsernameOrPassword" = "Nome de usuário, senha ou código de dois fatores inválido." "successLogin" = "Você entrou na sua conta com sucesso." [pages.index] "title" = "Visão Geral" "cpu" = "CPU" "logicalProcessors" = "Processadores lógicos" "frequency" = "Frequência" "swap" = "Swap" "storage" = "Armazenamento" "memory" = "RAM" "threads" = "Threads" "xrayStatus" = "Xray" "stopXray" = "Parar" "restartXray" = "Reiniciar" "xraySwitch" = "Versão" "xraySwitchClick" = "Escolha a versão para a qual deseja alternar." "xraySwitchClickDesk" = "Escolha com cuidado, pois versões mais antigas podem não ser compatíveis com as configurações atuais." "xrayStatusUnknown" = "Desconhecido" "xrayStatusRunning" = "Em execução" "xrayStatusStop" = "Parado" "xrayStatusError" = "Erro" "xrayErrorPopoverTitle" = "Ocorreu um erro ao executar o Xray" "operationHours" = "Tempo de Atividade" "systemLoad" = "Carga do Sistema" "systemLoadDesc" = "Média de carga do sistema nos últimos 1, 5 e 15 minutos" "connectionCount" = "Estatísticas de Conexão" "ipAddresses" = "Endereços IP" "toggleIpVisibility" = "Alternar visibilidade do IP" "overallSpeed" = "Velocidade geral" "upload" = "Upload" "download" = "Download" "totalData" = "Dados totais" "sent" = "Enviado" "received" = "Recebido" "documentation" = "Documentação" "xraySwitchVersionDialog" = "Você realmente deseja alterar a versão do Xray?" "xraySwitchVersionDialogDesc" = "Isso mudará a versão do Xray para #version#." "xraySwitchVersionPopover" = "Xray atualizado com sucesso" "geofileUpdateDialog" = "Você realmente deseja atualizar o geofile?" "geofileUpdateDialogDesc" = "Isso atualizará o arquivo #filename#." "geofilesUpdateDialogDesc" = "Isso atualizará todos os arquivos." "geofilesUpdateAll" = "Atualizar tudo" "geofileUpdatePopover" = "Geofile atualizado com sucesso" "dontRefresh" = "Instalação em andamento, por favor não atualize a página" "logs" = "Logs" "config" = "Configuração" "backup" = "Backup" "backupTitle" = "Backup e Restauração do Banco de Dados" "exportDatabase" = "Backup" "exportDatabaseDesc" = "Clique para baixar um arquivo .db contendo um backup do seu banco de dados atual para o seu dispositivo." "importDatabase" = "Restaurar" "importDatabaseDesc" = "Clique para selecionar e enviar um arquivo .db do seu dispositivo para restaurar seu banco de dados a partir de um backup." "importDatabaseSuccess" = "O banco de dados foi importado com sucesso" "importDatabaseError" = "Ocorreu um erro ao importar o banco de dados" "readDatabaseError" = "Ocorreu um erro ao ler o banco de dados" "getDatabaseError" = "Ocorreu um erro ao recuperar o banco de dados" "getConfigError" = "Ocorreu um erro ao recuperar o arquivo de configuração" [pages.inbounds] "allTimeTraffic" = "Tráfego Total" "allTimeTrafficUsage" = "Uso total de todos os tempos" "title" = "Inbounds" "totalDownUp" = "Total Enviado/Recebido" "totalUsage" = "Uso Total" "inboundCount" = "Total de Inbounds" "operate" = "Menu" "enable" = "Ativado" "remark" = "Observação" "protocol" = "Protocolo" "port" = "Porta" "portMap" = "Porta Mapeada" "traffic" = "Tráfego" "details" = "Detalhes" "transportConfig" = "Transporte" "expireDate" = "Duração" "createdAt" = "Criado" "updatedAt" = "Atualizado" "resetTraffic" = "Redefinir Tráfego" "addInbound" = "Adicionar Inbound" "generalActions" = "Ações Gerais" "autoRefresh" = "Atualização automática" "autoRefreshInterval" = "Intervalo" "modifyInbound" = "Modificar Inbound" "deleteInbound" = "Excluir Inbound" "deleteInboundContent" = "Tem certeza de que deseja excluir o inbound?" "deleteClient" = "Excluir Cliente" "deleteClientContent" = "Tem certeza de que deseja excluir o cliente?" "resetTrafficContent" = "Tem certeza de que deseja redefinir o tráfego?" "copyLink" = "Copiar URL" "address" = "Endereço" "network" = "Rede" "destinationPort" = "Porta de Destino" "targetAddress" = "Endereço de Destino" "monitorDesc" = "Deixe em branco para ouvir todos os IPs" "meansNoLimit" = "= Ilimitado. (unidade: GB)" "totalFlow" = "Fluxo Total" "leaveBlankToNeverExpire" = "Deixe em branco para nunca expirar" "noRecommendKeepDefault" = "Recomenda-se manter o padrão" "certificatePath" = "Caminho" "certificateContent" = "Conteúdo" "publicKey" = "Chave Pública" "privatekey" = "Chave Privada" "clickOnQRcode" = "Clique no Código QR para Copiar" "client" = "Cliente" "export" = "Exportar Todos os URLs" "clone" = "Clonar" "cloneInbound" = "Clonar" "cloneInboundContent" = "Todas as configurações deste inbound, exceto Porta, IP de Escuta e Clientes, serão aplicadas ao clone." "cloneInboundOk" = "Clonar" "resetAllTraffic" = "Redefinir Tráfego de Todos os Inbounds" "resetAllTrafficTitle" = "Redefinir Tráfego de Todos os Inbounds" "resetAllTrafficContent" = "Tem certeza de que deseja redefinir o tráfego de todos os inbounds?" "resetInboundClientTraffics" = "Redefinir Tráfego dos Clientes" "resetInboundClientTrafficTitle" = "Redefinir Tráfego dos Clientes" "resetInboundClientTrafficContent" = "Tem certeza de que deseja redefinir o tráfego dos clientes deste inbound?" "resetAllClientTraffics" = "Redefinir Tráfego de Todos os Clientes" "resetAllClientTrafficTitle" = "Redefinir Tráfego de Todos os Clientes" "resetAllClientTrafficContent" = "Tem certeza de que deseja redefinir o tráfego de todos os clientes?" "delDepletedClients" = "Excluir Clientes Esgotados" "delDepletedClientsTitle" = "Excluir Clientes Esgotados" "delDepletedClientsContent" = "Tem certeza de que deseja excluir todos os clientes esgotados?" "email" = "Email" "emailDesc" = "Por favor, forneça um endereço de e-mail único." "IPLimit" = "Limite de IP" "IPLimitDesc" = "Desativa o inbound se o número ultrapassar o valor definido. (0 = desativar)" "IPLimitlog" = "Log de IP" "IPLimitlogDesc" = "O histórico de IPs. (para ativar o inbound após a desativação, limpe o log)" "IPLimitlogclear" = "Limpar o Log" "setDefaultCert" = "Definir Certificado pelo Painel" "telegramDesc" = "Por favor, forneça o ID do Chat do Telegram. (use o comando '/id' no bot) ou (@userinfobot)" "subscriptionDesc" = "Para encontrar seu URL de assinatura, navegue até 'Detalhes'. Além disso, você pode usar o mesmo nome para vários clientes." "info" = "Informações" "same" = "Igual" "inboundData" = "Dados do Inbound" "exportInbound" = "Exportar Inbound" "import" = "Importar" "importInbound" = "Importar um Inbound" "periodicTrafficResetTitle" = "Reset de Tráfego" "periodicTrafficResetDesc" = "Reinicia automaticamente o contador de tráfego em intervalos especificados" "lastReset" = "Último Reset" [pages.client] "add" = "Adicionar Cliente" "edit" = "Editar Cliente" "submitAdd" = "Adicionar Cliente" "submitEdit" = "Salvar Alterações" "clientCount" = "Número de Clientes" "bulk" = "Adicionar Vários" "method" = "Método" "first" = "Primeiro" "last" = "Último" "prefix" = "Prefixo" "postfix" = "Sufixo" "delayedStart" = "Iniciar Após Primeiro Uso" "expireDays" = "Duração" "days" = "Dia(s)" "renew" = "Renovação Automática" "renewDesc" = "Renovação automática após expiração. (0 = desativado)(unidade: dia)" [pages.inbounds.periodicTrafficReset] "never" = "Nunca" "daily" = "Diariamente" "weekly" = "Semanalmente" "monthly" = "Mensalmente" [pages.inbounds.toasts] "obtain" = "Obter" "updateSuccess" = "A atualização foi bem-sucedida" "logCleanSuccess" = "O log foi limpo" "inboundsUpdateSuccess" = "Entradas atualizadas com sucesso" "inboundUpdateSuccess" = "Entrada atualizada com sucesso" "inboundCreateSuccess" = "Entrada criada com sucesso" "inboundDeleteSuccess" = "Entrada excluída com sucesso" "inboundClientAddSuccess" = "Cliente(s) de entrada adicionado(s)" "inboundClientDeleteSuccess" = "Cliente de entrada excluído" "inboundClientUpdateSuccess" = "Cliente de entrada atualizado" "delDepletedClientsSuccess" = "Todos os clientes esgotados foram excluídos" "resetAllClientTrafficSuccess" = "Todo o tráfego do cliente foi reiniciado" "resetAllTrafficSuccess" = "Todo o tráfego foi reiniciado" "resetInboundClientTrafficSuccess" = "O tráfego foi reiniciado" "trafficGetError" = "Erro ao obter tráfegos" "getNewX25519CertError" = "Erro ao obter o certificado X25519." "getNewmldsa65Error" = "Erro ao obter o certificado mldsa65." "getNewVlessEncError" = "Erro ao obter o certificado VlessEnc." [pages.inbounds.stream.general] "request" = "Requisição" "response" = "Resposta" "name" = "Nome" "value" = "Valor" [pages.inbounds.stream.tcp] "version" = "Versão" "method" = "Método" "path" = "Caminho" "status" = "Status" "statusDescription" = "Descrição do Status" "requestHeader" = "Cabeçalho da Requisição" "responseHeader" = "Cabeçalho da Resposta" [pages.settings] "title" = "Configurações do Painel" "save" = "Salvar" "infoDesc" = "Toda alteração feita aqui precisa ser salva. Reinicie o painel para aplicar as alterações." "restartPanel" = "Reiniciar Painel" "restartPanelDesc" = "Tem certeza de que deseja reiniciar o painel? Se não conseguir acessar o painel após reiniciar, consulte os logs do painel no servidor." "restartPanelSuccess" = "O painel foi reiniciado com sucesso" "actions" = "Ações" "resetDefaultConfig" = "Redefinir para Padrão" "panelSettings" = "Geral" "securitySettings" = "Autenticação" "TGBotSettings" = "Bot do Telegram" "panelListeningIP" = "IP de Escuta" "panelListeningIPDesc" = "O endereço IP para o painel web. (deixe em branco para escutar em todos os IPs)" "panelListeningDomain" = "Domínio de Escuta" "panelListeningDomainDesc" = "O nome de domínio para o painel web. (deixe em branco para escutar em todos os domínios e IPs)" "panelPort" = "Porta de Escuta" "panelPortDesc" = "O número da porta para o painel web. (deve ser uma porta não usada)" "publicKeyPath" = "Caminho da Chave Pública" "publicKeyPathDesc" = "O caminho do arquivo de chave pública para o painel web. (começa com ‘/‘)" "privateKeyPath" = "Caminho da Chave Privada" "privateKeyPathDesc" = "O caminho do arquivo de chave privada para o painel web. (começa com ‘/‘)" "panelUrlPath" = "Caminho URI" "panelUrlPathDesc" = "O caminho URI para o painel web. (começa com ‘/‘ e termina com ‘/‘)" "pageSize" = "Tamanho da Paginação" "pageSizeDesc" = "Definir o tamanho da página para a tabela de entradas. (0 = desativado)" "remarkModel" = "Modelo de Observação & Caractere de Separação" "datepicker" = "Tipo de Calendário" "datepickerPlaceholder" = "Selecionar data" "datepickerDescription" = "Tarefas agendadas serão executadas com base neste calendário." "sampleRemark" = "Exemplo de Observação" "oldUsername" = "Nome de Usuário Atual" "currentPassword" = "Senha Atual" "newUsername" = "Novo Nome de Usuário" "newPassword" = "Nova Senha" "telegramBotEnable" = "Ativar Bot do Telegram" "telegramBotEnableDesc" = "Ativa o bot do Telegram." "telegramToken" = "Token do Telegram" "telegramTokenDesc" = "O token do bot do Telegram obtido de '@BotFather'." "telegramProxy" = "Proxy SOCKS" "telegramProxyDesc" = "Ativa o proxy SOCKS5 para conectar ao Telegram. (ajuste as configurações conforme o guia)" "telegramAPIServer" = "API Server do Telegram" "telegramAPIServerDesc" = "O servidor API do Telegram a ser usado. Deixe em branco para usar o servidor padrão." "telegramChatId" = "ID de Chat do Administrador" "telegramChatIdDesc" = "O(s) ID(s) de Chat do Administrador no Telegram. (separado por vírgulas)(obtenha aqui @userinfobot) ou (use o comando '/id' no bot)" "telegramNotifyTime" = "Hora da Notificação" "telegramNotifyTimeDesc" = "O horário de notificação do bot do Telegram configurado para relatórios periódicos. (use o formato de tempo do crontab)" "tgNotifyBackup" = "Backup do Banco de Dados" "tgNotifyBackupDesc" = "Enviar arquivo de backup do banco de dados junto com o relatório." "tgNotifyLogin" = "Notificação de Login" "tgNotifyLoginDesc" = "Receba notificações sobre o nome de usuário, endereço IP e horário sempre que alguém tentar fazer login no seu painel web." "sessionMaxAge" = "Duração da Sessão" "sessionMaxAgeDesc" = "A duração pela qual você pode permanecer logado. (unidade: minuto)" "expireTimeDiff" = "Notificação de Expiração" "expireTimeDiffDesc" = "Receba notificações sobre a data de expiração ao atingir esse limite. (unidade: dia)" "trafficDiff" = "Notificação de Limite de Tráfego" "trafficDiffDesc" = "Receba notificações sobre o limite de tráfego ao atingir esse limite. (unidade: GB)" "tgNotifyCpu" = "Notificação de Carga da CPU" "tgNotifyCpuDesc" = "Receba notificações se a carga da CPU ultrapassar esse limite. (unidade: %)" "timeZone" = "Fuso Horário" "timeZoneDesc" = "As tarefas agendadas serão executadas com base nesse fuso horário." "subSettings" = "Assinatura" "subEnable" = "Ativar Serviço de Assinatura" "subEnableDesc" = "Ativa o serviço de assinatura." "subJsonEnable" = "Ativar/Desativar o endpoint de assinatura JSON de forma independente." "subTitle" = "Título da Assinatura" "subTitleDesc" = "Título exibido no cliente VPN" "subSupportUrl" = "URL de Suporte" "subSupportUrlDesc" = "Link de suporte técnico exibido no cliente VPN" "subProfileUrl" = "URL de Perfil" "subProfileUrlDesc" = "Um link para o seu site exibido no cliente VPN" "subAnnounce" = "Anúncio" "subAnnounceDesc" = "O texto do anúncio exibido no cliente VPN" "subEnableRouting" = "Ativar roteamento" "subEnableRoutingDesc" = "Configuração global para habilitar o roteamento no cliente VPN. (Apenas para Happ)" "subRoutingRules" = "Regras de roteamento" "subRoutingRulesDesc" = "Regras de roteamento globais para o cliente VPN. (Apenas para Happ)" "subListen" = "IP de Escuta" "subListenDesc" = "O endereço IP para o serviço de assinatura. (deixe em branco para escutar em todos os IPs)" "subPort" = "Porta de Escuta" "subPortDesc" = "O número da porta para o serviço de assinatura. (deve ser uma porta não usada)" "subCertPath" = "Caminho da Chave Pública" "subCertPathDesc" = "O caminho do arquivo de chave pública para o serviço de assinatura. (começa com ‘/‘)" "subKeyPath" = "Caminho da Chave Privada" "subKeyPathDesc" = "O caminho do arquivo de chave privada para o serviço de assinatura. (começa com ‘/‘)" "subPath" = "Caminho URI" "subPathDesc" = "O caminho URI para o serviço de assinatura. (começa com ‘/‘ e termina com ‘/‘)" "subDomain" = "Domínio de Escuta" "subDomainDesc" = "O nome de domínio para o serviço de assinatura. (deixe em branco para escutar em todos os domínios e IPs)" "subUpdates" = "Intervalos de Atualização" "subUpdatesDesc" = "Os intervalos de atualização da URL de assinatura nos aplicativos de cliente. (unidade: hora)" "subEncrypt" = "Codificar" "subEncryptDesc" = "O conteúdo retornado pelo serviço de assinatura será codificado em Base64." "subShowInfo" = "Mostrar Informações de Uso" "subShowInfoDesc" = "O tráfego restante e a data serão exibidos nos aplicativos de cliente." "subURI" = "URI de Proxy Reverso" "subURIDesc" = "O caminho URI da URL de assinatura para uso por trás de proxies." "externalTrafficInformEnable" = "Informações de tráfego externo" "externalTrafficInformEnableDesc" = "Informar a API externa sobre cada atualização de tráfego." "externalTrafficInformURI" = "URI de informação de tráfego externo" "externalTrafficInformURIDesc" = "As atualizações de tráfego são enviadas para este URI." "fragment" = "Fragmentação" "fragmentDesc" = "Ativa a fragmentação para o pacote TLS hello." "fragmentSett" = "Configurações de Fragmentação" "noisesDesc" = "Ativar Noises." "noisesSett" = "Configurações de Noises" "mux" = "Mux" "muxDesc" = "Transmitir múltiplos fluxos de dados independentes dentro de um fluxo de dados estabelecido." "muxSett" = "Configurações de Mux" "direct" = "Conexão Direta" "directDesc" = "Estabelece conexões diretamente com domínios ou intervalos de IP de um país específico." "notifications" = "Notificações" "certs" = "Certificados" "externalTraffic" = "Tráfego Externo" "dateAndTime" = "Data e Hora" "proxyAndServer" = "Proxy e Servidor" "intervals" = "Intervalos" "information" = "Informação" "language" = "Idioma" "telegramBotLanguage" = "Idioma do Bot do Telegram" [pages.xray] "title" = "Configurações Xray" "save" = "Salvar" "restart" = "Reiniciar Xray" "restartSuccess" = "Xray foi reiniciado com sucesso" "stopSuccess" = "Xray foi interrompido com sucesso" "restartError" = "Ocorreu um erro ao reiniciar o Xray." "stopError" = "Ocorreu um erro ao parar o Xray." "basicTemplate" = "Básico" "advancedTemplate" = "Avançado" "generalConfigs" = "Geral" "generalConfigsDesc" = "Essas opções determinam ajustes gerais." "logConfigs" = "Log" "logConfigsDesc" = "Os logs podem afetar a eficiência do servidor. É recomendável habilitá-los com sabedoria apenas se necessário." "blockConfigsDesc" = "Essas opções bloqueiam tráfego com base em protocolos e sites específicos solicitados." "basicRouting" = "Roteamento Básico" "blockConnectionsConfigsDesc" = "Essas opções bloquearão o tráfego com base no país solicitado." "directConnectionsConfigsDesc" = "Uma conexão direta garante que o tráfego específico não seja roteado por outro servidor." "blockips" = "Bloquear IPs" "blockdomains" = "Bloquear Domínios" "directips" = "IPs Diretos" "directdomains" = "Domínios Diretos" "ipv4Routing" = "Roteamento IPv4" "ipv4RoutingDesc" = "Essas opções roteam o tráfego para um destino específico via IPv4." "warpRouting" = "Roteamento WARP" "warpRoutingDesc" = "Essas opções roteam o tráfego para um destino específico via WARP." "Template" = "Modelo de Configuração Avançada do Xray" "TemplateDesc" = "O arquivo final de configuração do Xray será gerado com base neste modelo." "FreedomStrategy" = "Estratégia do Protocolo Freedom" "FreedomStrategyDesc" = "Definir a estratégia de saída para a rede no Protocolo Freedom." "RoutingStrategy" = "Estratégia Geral de Roteamento" "RoutingStrategyDesc" = "Definir a estratégia geral de roteamento de tráfego para resolver todas as solicitações." "outboundTestUrl" = "URL de teste de outbound" "outboundTestUrlDesc" = "URL usada ao testar conectividade do outbound" "Torrent" = "Bloquear Protocolo BitTorrent" "Inbounds" = "Inbounds" "InboundsDesc" = "Aceitar clientes específicos." "Outbounds" = "Outbounds" "Balancers" = "Balanceadores" "OutboundsDesc" = "Definir o caminho de saída do tráfego." "Routings" = "Regras de Roteamento" "RoutingsDesc" = "A prioridade de cada regra é importante!" "completeTemplate" = "Todos" "logLevel" = "Nível de Log" "logLevelDesc" = "O nível de log para erros, indicando a informação que precisa ser registrada." "accessLog" = "Log de Acesso" "accessLogDesc" = "O caminho do arquivo para o log de acesso. O valor especial 'none' desativa os logs de acesso." "errorLog" = "Log de Erros" "errorLogDesc" = "O caminho do arquivo para o log de erros. O valor especial 'none' desativa os logs de erro." "dnsLog" = "Log DNS" "dnsLogDesc" = "Se ativar logs de consulta DNS" "maskAddress" = "Mascarar Endereço" "maskAddressDesc" = "Máscara de endereço IP, quando ativado, substitui automaticamente o endereço IP que aparece no log." "statistics" = "Estatísticas" "statsInboundUplink" = "Estatísticas de Upload de Entrada" "statsInboundUplinkDesc" = "Habilita a coleta de estatísticas para o tráfego de upload de todos os proxies de entrada." "statsInboundDownlink" = "Estatísticas de Download de Entrada" "statsInboundDownlinkDesc" = "Habilita a coleta de estatísticas para o tráfego de download de todos os proxies de entrada." "statsOutboundUplink" = "Estatísticas de Upload de Saída" "statsOutboundUplinkDesc" = "Habilita a coleta de estatísticas para o tráfego de upload de todos os proxies de saída." "statsOutboundDownlink" = "Estatísticas de Download de Saída" "statsOutboundDownlinkDesc" = "Habilita a coleta de estatísticas para o tráfego de download de todos os proxies de saída." [pages.xray.rules] "first" = "Primeiro" "last" = "Último" "up" = "Cima" "down" = "Baixo" "source" = "Fonte" "dest" = "Destino" "inbound" = "Entrada" "outbound" = "Saída" "balancer" = "Balanceador" "info" = "Info" "add" = "Adicionar Regra" "edit" = "Editar Regra" "useComma" = "Itens separados por vírgula" [pages.xray.outbound] "addOutbound" = "Adicionar Saída" "addReverse" = "Adicionar Reverso" "editOutbound" = "Editar Saída" "editReverse" = "Editar Reverso" "tag" = "Tag" "tagDesc" = "Tag Única" "address" = "Endereço" "reverse" = "Reverso" "domain" = "Domínio" "type" = "Tipo" "bridge" = "Ponte" "portal" = "Portal" "link" = "Link" "intercon" = "Interconexão" "settings" = "Configurações" "accountInfo" = "Informações da Conta" "outboundStatus" = "Status de Saída" "sendThrough" = "Enviar Através de" "test" = "Testar" "testResult" = "Resultado do teste" "testing" = "Testando conexão..." "testSuccess" = "Teste bem-sucedido" "testFailed" = "Teste falhou" "testError" = "Falha ao testar saída" [pages.xray.balancer] "addBalancer" = "Adicionar Balanceador" "editBalancer" = "Editar Balanceador" "balancerStrategy" = "Estratégia" "balancerSelectors" = "Seletores" "tag" = "Tag" "tagDesc" = "Tag Única" "balancerDesc" = "Não é possível usar balancerTag e outboundTag ao mesmo tempo. Se usados simultaneamente, apenas outboundTag funcionará." [pages.xray.wireguard] "secretKey" = "Chave Secreta" "publicKey" = "Chave Pública" "allowedIPs" = "IPs Permitidos" "endpoint" = "Ponto Final" "psk" = "Chave Pré-Compartilhada" "domainStrategy" = "Estratégia de Domínio" [pages.xray.tun] "nameDesc" = "O nome da interface TUN. O padrão é 'xray0'" "mtuDesc" = "Unidade Máxima de Transmissão. O tamanho máximo dos pacotes de dados. O padrão é 1500" "userLevel" = "Nível do Usuário" "userLevelDesc" = "Todas as conexões feitas através deste inbound usarão este nível de usuário. O padrão é 0" [pages.xray.dns] "enable" = "Ativar DNS" "enableDesc" = "Ativar o servidor DNS integrado" "tag" = "Tag de Entrada DNS" "tagDesc" = "Esta tag estará disponível como uma tag de Entrada nas regras de roteamento." "clientIp" = "IP do Cliente" "clientIpDesc" = "Usado para notificar o servidor sobre a localização IP especificada durante consultas DNS" "disableCache" = "Desativar cache" "disableCacheDesc" = "Desativa o cache de DNS" "disableFallback" = "Desativar Fallback" "disableFallbackDesc" = "Desativa consultas DNS de fallback" "disableFallbackIfMatch" = "Desativar Fallback Se Corresponder" "disableFallbackIfMatchDesc" = "Desativa consultas DNS de fallback quando a lista de domínios correspondentes do servidor DNS é atingida" "enableParallelQuery" = "Habilitar Consulta Paralela" "enableParallelQueryDesc" = "Habilitar consultas DNS paralelas para múltiplos servidores para resolução mais rápida" "strategy" = "Estratégia de Consulta" "strategyDesc" = "Estratégia geral para resolver nomes de domínio" "add" = "Adicionar Servidor" "edit" = "Editar Servidor" "domains" = "Domínios" "expectIPs" = "IPs Esperadas" "unexpectIPs" = "IPs inesperados" "useSystemHosts" = "Usar Hosts do sistema" "useSystemHostsDesc" = "Usar o arquivo hosts de um sistema instalado" "usePreset" = "Usar modelo" "dnsPresetTitle" = "Modelos DNS" "dnsPresetFamily" = "Familiar" [pages.xray.fakedns] "add" = "Adicionar Fake DNS" "edit" = "Editar Fake DNS" "ipPool" = "Sub-rede do Pool de IP" "poolSize" = "Tamanho do Pool" [pages.settings.security] "admin" = "Credenciais de administrador" "twoFactor" = "Autenticação de dois fatores" "twoFactorEnable" = "Ativar 2FA" "twoFactorEnableDesc" = "Adiciona uma camada extra de autenticação para mais segurança." "twoFactorModalSetTitle" = "Ativar autenticação de dois fatores" "twoFactorModalDeleteTitle" = "Desativar autenticação de dois fatores" "twoFactorModalSteps" = "Para configurar a autenticação de dois fatores, siga alguns passos:" "twoFactorModalFirstStep" = "1. Escaneie este QR code no aplicativo de autenticação ou copie o token próximo ao QR code e cole no aplicativo" "twoFactorModalSecondStep" = "2. Digite o código do aplicativo" "twoFactorModalRemoveStep" = "Digite o código do aplicativo para remover a autenticação de dois fatores." "twoFactorModalChangeCredentialsTitle" = "Alterar credenciais" "twoFactorModalChangeCredentialsStep" = "Insira o código do aplicativo para alterar as credenciais do administrador." "twoFactorModalSetSuccess" = "A autenticação de dois fatores foi estabelecida com sucesso" "twoFactorModalDeleteSuccess" = "A autenticação de dois fatores foi excluída com sucesso" "twoFactorModalError" = "Código incorreto" [pages.settings.toasts] "modifySettings" = "Os parâmetros foram alterados." "getSettings" = "Ocorreu um erro ao recuperar os parâmetros." "modifyUserError" = "Ocorreu um erro ao alterar as credenciais do administrador." "modifyUser" = "Você alterou com sucesso as credenciais do administrador." "originalUserPassIncorrect" = "O nome de usuário ou senha atual é inválido" "userPassMustBeNotEmpty" = "O novo nome de usuário e senha não podem estar vazios" "getOutboundTrafficError" = "Erro ao obter tráfego de saída" "resetOutboundTrafficError" = "Erro ao redefinir tráfego de saída" [tgbot] "keyboardClosed" = "❌ Teclado fechado!" "noResult" = "❗ Nenhum resultado!" "noQuery" = "❌ Consulta não encontrada! Por favor, use o comando novamente!" "wentWrong" = "❌ Algo deu errado!" "noIpRecord" = "❗ Nenhum registro de IP!" "noInbounds" = "❗ Nenhum inbound encontrado!" "unlimited" = "♾ Ilimitado (Reset)" "add" = "Adicionar" "month" = "Mês" "months" = "Meses" "day" = "Dia" "days" = "Dias" "hours" = "Horas" "minutes" = "Minutos" "unknown" = "Desconhecido" "inbounds" = "Inbounds" "clients" = "Clientes" "offline" = "🔴 Offline" "online" = "🟢 Online" [tgbot.commands] "unknown" = "❗ Comando desconhecido." "pleaseChoose" = "👇 Escolha:\r\n" "help" = "🤖 Bem-vindo a este bot! Ele foi projetado para oferecer dados específicos do painel da web e permite que você faça as modificações necessárias.\r\n\r\n" "start" = "👋 Olá {{ .Firstname }}.\r\n" "welcome" = "🤖 Bem-vindo ao bot de gerenciamento do {{ .Hostname }}.\r\n" "status" = "✅ Bot está OK!" "usage" = "❗ Por favor, forneça um texto para pesquisar!" "getID" = "🆔 Seu ID: {{ .ID }}" "helpAdminCommands" = "Para reiniciar o Xray Core:\r\n/restart\r\n\r\nPara pesquisar por um email de cliente:\r\n/usage [Email]\r\n\r\nPara pesquisar por inbounds (com estatísticas do cliente):\r\n/inbound [Remark]\r\n\r\nTelegram Chat ID:\r\n/id" "helpClientCommands" = "Para pesquisar por estatísticas, use o seguinte comando:\r\n\r\n/usage [Email]\r\n\r\nTelegram Chat ID:\r\n/id" "restartUsage" = "\r\n\r\n/restart" "restartSuccess" = "✅ Operação bem-sucedida!" "restartFailed" = "❗ Erro na operação.\r\n\r\nErro: {{ .Error }}." "xrayNotRunning" = "❗ Xray Core não está em execução." "startDesc" = "Mostrar menu principal" "helpDesc" = "Ajuda do bot" "statusDesc" = "Verificar status do bot" "idDesc" = "Mostrar seu ID do Telegram" [tgbot.messages] "cpuThreshold" = "🔴 A carga da CPU {{ .Percent }}% excede o limite de {{ .Threshold }}%" "selectUserFailed" = "❌ Erro na seleção do usuário!" "userSaved" = "✅ Usuário do Telegram salvo." "loginSuccess" = "✅ Conectado ao painel com sucesso.\r\n" "loginFailed" = "❗️Tentativa de login no painel falhou.\r\n" "2faFailed" = "Falha no 2FA" "report" = "🕰 Relatórios agendados: {{ .RunTime }}\r\n" "datetime" = "⏰ Data&Hora: {{ .DateTime }}\r\n" "hostname" = "💻 Host: {{ .Hostname }}\r\n" "version" = "🚀 Versão 3X-UI: {{ .Version }}\r\n" "xrayVersion" = "📡 Versão Xray: {{ .XrayVersion }}\r\n" "ipv6" = "🌐 IPv6: {{ .IPv6 }}\r\n" "ipv4" = "🌐 IPv4: {{ .IPv4 }}\r\n" "ip" = "🌐 IP: {{ .IP }}\r\n" "ips" = "🔢 IPs:\r\n{{ .IPs }}\r\n" "serverUpTime" = "⏳ Tempo de atividade: {{ .UpTime }} {{ .Unit }}\r\n" "serverLoad" = "📈 Carga do sistema: {{ .Load1 }}, {{ .Load2 }}, {{ .Load3 }}\r\n" "serverMemory" = "📋 RAM: {{ .Current }}/{{ .Total }}\r\n" "tcpCount" = "🔹 TCP: {{ .Count }}\r\n" "udpCount" = "🔸 UDP: {{ .Count }}\r\n" "traffic" = "🚦 Tráfego: {{ .Total }} (↑{{ .Upload }},↓{{ .Download }})\r\n" "xrayStatus" = "ℹ️ Status: {{ .State }}\r\n" "username" = "👤 Nome de usuário: {{ .Username }}\r\n" "password" = "👤 Senha: {{ .Password }}\r\n" "time" = "⏰ Hora: {{ .Time }}\r\n" "inbound" = "📍 Inbound: {{ .Remark }}\r\n" "port" = "🔌 Porta: {{ .Port }}\r\n" "expire" = "📅 Data de expiração: {{ .Time }}\r\n" "expireIn" = "📅 Expira em: {{ .Time }}\r\n" "active" = "💡 Ativo: {{ .Enable }}\r\n" "enabled" = "🚨 Ativado: {{ .Enable }}\r\n" "online" = "🌐 Status da conexão: {{ .Status }}\r\n" "lastOnline" = "🔙 Última vez online: {{ .Time }}\r\n" "email" = "📧 Email: {{ .Email }}\r\n" "upload" = "🔼 Upload: ↑{{ .Upload }}\r\n" "download" = "🔽 Download: ↓{{ .Download }}\r\n" "total" = "📊 Total: ↑↓{{ .UpDown }} / {{ .Total }}\r\n" "TGUser" = "👤 Usuário do Telegram: {{ .TelegramID }}\r\n" "exhaustedMsg" = "🚨 {{ .Type }} esgotado:\r\n" "exhaustedCount" = "🚨 Contagem de {{ .Type }} esgotado:\r\n" "onlinesCount" = "🌐 Clientes online: {{ .Count }}\r\n" "disabled" = "🛑 Desativado: {{ .Disabled }}\r\n" "depleteSoon" = "🔜 Esgotar em breve: {{ .Deplete }}\r\n\r\n" "backupTime" = "🗄 Hora do backup: {{ .Time }}\r\n" "refreshedOn" = "\r\n📋🔄 Atualizado em: {{ .Time }}\r\n\r\n" "yes" = "✅ Sim" "no" = "❌ Não" "received_id" = "🔑📥 ID atualizado." "received_password" = "🔑📥 Senha atualizada." "received_email" = "📧📥 E-mail atualizado." "received_comment" = "💬📥 Comentário atualizado." "id_prompt" = "🔑 ID Padrão: {{ .ClientId }}\n\nDigite seu ID." "pass_prompt" = "🔑 Senha Padrão: {{ .ClientPassword }}\n\nDigite sua senha." "email_prompt" = "📧 E-mail Padrão: {{ .ClientEmail }}\n\nDigite seu e-mail." "comment_prompt" = "💬 Comentário Padrão: {{ .ClientComment }}\n\nDigite seu comentário." "inbound_client_data_id" = "🔄 Entrada: {{ .InboundRemark }}\n\n🔑 ID: {{ .ClientId }}\n📧 Email: {{ .ClientEmail }}\n📊 Tráfego: {{ .ClientTraffic }}\n📅 Data de expiração: {{ .ClientExp }}\n🌐 Limite de IP: {{ .IpLimit }}\n💬 Comentário: {{ .ClientComment }}\n\nAgora você pode adicionar o cliente à entrada!" "inbound_client_data_pass" = "🔄 Entrada: {{ .InboundRemark }}\n\n🔑 Senha: {{ .ClientPass }}\n📧 Email: {{ .ClientEmail }}\n📊 Tráfego: {{ .ClientTraffic }}\n📅 Data de expiração: {{ .ClientExp }}\n🌐 Limite de IP: {{ .IpLimit }}\n💬 Comentário: {{ .ClientComment }}\n\nAgora você pode adicionar o cliente à entrada!" "cancel" = "❌ Processo Cancelado! \n\nVocê pode iniciar novamente a qualquer momento com /start. 🔄" "error_add_client" = "⚠️ Erro:\n\n {{ .error }}" "using_default_value" = "Tudo bem, vou manter o valor padrão. 😊" "incorrect_input" = "Sua entrada não é válida.\nAs frases devem ser contínuas, sem espaços.\nExemplo correto: aaaaaa\nExemplo incorreto: aaa aaa 🚫" "AreYouSure" = "Você tem certeza? 🤔" "SuccessResetTraffic" = "📧 Email: {{ .ClientEmail }}\n🏁 Resultado: ✅ Sucesso" "FailedResetTraffic" = "📧 Email: {{ .ClientEmail }}\n🏁 Resultado: ❌ Falhou \n\n🛠️ Erro: [ {{ .ErrorMessage }} ]" "FinishProcess" = "🔚 Processo de redefinição de tráfego concluído para todos os clientes." [tgbot.buttons] "closeKeyboard" = "❌ Fechar teclado" "cancel" = "❌ Cancelar" "cancelReset" = "❌ Cancelar redefinição" "cancelIpLimit" = "❌ Cancelar limite de IP" "confirmResetTraffic" = "✅ Confirmar redefinição de tráfego?" "confirmClearIps" = "✅ Confirmar limpar IPs?" "confirmRemoveTGUser" = "✅ Confirmar remover usuário do Telegram?" "confirmToggle" = "✅ Confirmar ativar/desativar usuário?" "dbBackup" = "Obter backup do DB" "serverUsage" = "Uso do servidor" "getInbounds" = "Obter Inbounds" "depleteSoon" = "Esgotar em breve" "clientUsage" = "Obter uso" "onlines" = "Clientes online" "commands" = "Comandos" "refresh" = "🔄 Atualizar" "clearIPs" = "❌ Limpar IPs" "removeTGUser" = "❌ Remover usuário do Telegram" "selectTGUser" = "👤 Selecionar usuário do Telegram" "selectOneTGUser" = "👤 Selecione um usuário do Telegram:" "resetTraffic" = "📈 Redefinir tráfego" "resetExpire" = "📅 Alterar data de expiração" "ipLog" = "🔢 Log de IP" "ipLimit" = "🔢 Limite de IP" "setTGUser" = "👤 Definir usuário do Telegram" "toggle" = "🔘 Ativar / Desativar" "custom" = "🔢 Personalizado" "confirmNumber" = "✅ Confirmar: {{ .Num }}" "confirmNumberAdd" = "✅ Confirmar adicionar: {{ .Num }}" "limitTraffic" = "🚧 Limite de tráfego" "getBanLogs" = "Obter logs de banimento" "allClients" = "Todos os clientes" "addClient" = "Adicionar Cliente" "submitDisable" = "Enviar como Desativado ☑️" "submitEnable" = "Enviar como Ativado ✅" "use_default" = "🏷️ Usar padrão" "change_id" = "⚙️🔑 ID" "change_password" = "⚙️🔑 Senha" "change_email" = "⚙️📧 E-mail" "change_comment" = "⚙️💬 Comentário" "ResetAllTraffics" = "Redefinir Todo o Tráfego" "SortedTrafficUsageReport" = "Relatório de Uso de Tráfego Ordenado" [tgbot.answers] "successfulOperation" = "✅ Operação bem-sucedida!" "errorOperation" = "❗ Erro na operação." "getInboundsFailed" = "❌ Falha ao obter inbounds." "getClientsFailed" = "❌ Falha ao obter clientes." "canceled" = "❌ {{ .Email }}: Operação cancelada." "clientRefreshSuccess" = "✅ {{ .Email }}: Cliente atualizado com sucesso." "IpRefreshSuccess" = "✅ {{ .Email }}: IPs atualizados com sucesso." "TGIdRefreshSuccess" = "✅ {{ .Email }}: Usuário do Telegram do cliente atualizado com sucesso." "resetTrafficSuccess" = "✅ {{ .Email }}: Tráfego redefinido com sucesso." "setTrafficLimitSuccess" = "✅ {{ .Email }}: Limite de tráfego salvo com sucesso." "expireResetSuccess" = "✅ {{ .Email }}: Dias de expiração redefinidos com sucesso." "resetIpSuccess" = "✅ {{ .Email }}: Limite de IP {{ .Count }} salvo com sucesso." "clearIpSuccess" = "✅ {{ .Email }}: IPs limpos com sucesso." "getIpLog" = "✅ {{ .Email }}: Obter log de IP." "getUserInfo" = "✅ {{ .Email }}: Obter informações do usuário do Telegram." "removedTGUserSuccess" = "✅ {{ .Email }}: Usuário do Telegram removido com sucesso." "enableSuccess" = "✅ {{ .Email }}: Ativado com sucesso." "disableSuccess" = "✅ {{ .Email }}: Desativado com sucesso." "askToAddUserId" = "Sua configuração não foi encontrada!\r\nPeça ao seu administrador para usar seu Telegram ChatID em suas configurações.\r\n\r\nSeu ChatID: {{ .TgUserID }}" "chooseClient" = "Escolha um cliente para Inbound {{ .Inbound }}" "chooseInbound" = "Escolha um Inbound" ================================================ FILE: web/translation/translate.ru_RU.toml ================================================ "username" = "Имя пользователя" "password" = "Пароль" "login" = "Войти" "confirm" = "Подтвердить" "cancel" = "Отмена" "close" = "Закрыть" "create" = "Создать" "update" = "Обновить" "copy" = "Копировать" "copied" = "Скопировано" "download" = "Скачать" "remark" = "Примечание" "enable" = "Включить" "protocol" = "Протокол" "search" = "Поиск" "filter" = "Фильтр" "loading" = "Загрузка..." "second" = "Секунда" "minute" = "Минута" "hour" = "Час" "day" = "День" "check" = "Проверить" "indefinite" = "Бесконечно" "unlimited" = "Безлимит" "none" = "Пусто" "qrCode" = "QR-код" "info" = "Информация" "edit" = "Изменить" "delete" = "Удалить" "reset" = "Сбросить" "noData" = "Нет данных." "copySuccess" = "Скопировано" "sure" = "Да" "encryption" = "Шифрование" "useIPv4ForHost" = "Использовать IPv4 для подключения к хосту" "transmission" = "Транспорт" "host" = "Хост" "path" = "Путь" "camouflage" = "Маскировка" "status" = "Статус" "enabled" = "Включено" "disabled" = "Отключено" "depleted" = "Исчерпано" "depletingSoon" = "Почти исчерпано" "offline" = "Офлайн" "online" = "Онлайн" "domainName" = "Домен" "monitor" = "Мониторинг IP" "certificate" = "SSL-сертификат" "fail" = "Сбой" "comment" = "Комментарий" "success" = "Успешно" "lastOnline" = "Был(а) в сети" "getVersion" = "Узнать версию" "install" = "Установка" "clients" = "Клиенты" "usage" = "Использование" "twoFactorCode" = "Код 2FA" "remained" = "Остаток" "security" = "Безопасность" "secAlertTitle" = "Предупреждение системы безопасности" "secAlertSsl" = "Соединение не защищено. Не вводите конфиденциальные данные до установки SSL-сертификата." "secAlertConf" = "Некоторые настройки уязвимы. Рекомендуется усилить защиту для предотвращения атак." "secAlertSSL" = "Подключение к панели не защищено. Установите SSL-сертификат для защиты данных." "secAlertPanelPort" = "Порт панели по умолчанию небезопасен. Установите нестандартный или случайный порт." "secAlertPanelURI" = "Адрес панели по умолчанию небезопасен. Настройте уникальный и сложный URI." "secAlertSubURI" = "URI подписки по умолчанию небезопасен. Настройте уникальный и сложный адрес." "secAlertSubJsonURI" = "URI JSON-подписки по умолчанию небезопасен. Настройте уникальный и сложный адрес." "emptyDnsDesc" = "Нет добавленных DNS-серверов." "emptyFakeDnsDesc" = "Нет добавленных Fake DNS-серверов." "emptyBalancersDesc" = "Нет добавленных балансировщиков." "emptyReverseDesc" = "Нет добавленных реверс-прокси." "somethingWentWrong" = "Что-то пошло не так" [subscription] "title" = "Информация о подписке" "subId" = "ID подписки" "status" = "Статус" "downloaded" = "Загружено" "uploaded" = "Отправлено" "expiry" = "Срок действия" "totalQuota" = "Общий лимит" "individualLinks" = "Индивидуальные ссылки" "active" = "Активна" "inactive" = "Неактивна" "unlimited" = "Неограниченно" "noExpiry" = "Бессрочно" [menu] "theme" = "Тема" "dark" = "Темная" "ultraDark" = "Очень темная" "dashboard" = "Дашборд" "inbounds" = "Подключения" "settings" = "Настройки" "xray" = "Настройки Xray" "logout" = "Выход" "link" = "Управление" [pages.login] "hello" = "Привет!" "title" = "Добро пожаловать!" "loginAgain" = "Сессия истекла. Войдите в систему снова" [pages.login.toasts] "invalidFormData" = "Недопустимый формат данных" "emptyUsername" = "Введите имя пользователя" "emptyPassword" = "Введите пароль" "wrongUsernameOrPassword" = "Неверные данные учетной записи." "successLogin" = "Вход выполнен успешно" [pages.index] "title" = "Дашборд" "cpu" = "ЦП" "logicalProcessors" = "Логические процессоры" "frequency" = "Частота" "swap" = "Файл подкачки" "storage" = "Диск" "memory" = "ОЗУ" "threads" = "Потоки" "xrayStatus" = "Xray" "stopXray" = "Остановить" "restartXray" = "Перезапустить" "xraySwitch" = "Выбор версии" "xraySwitchClick" = "Выберите нужную версию" "xraySwitchClickDesk" = "Важно: старые версии могут не поддерживать текущие настройки" "xrayStatusUnknown" = "Неизвестно" "xrayStatusRunning" = "Запущен" "xrayStatusStop" = "Остановлен" "xrayStatusError" = "Ошибка" "xrayErrorPopoverTitle" = "Ошибка при запуске Xray" "operationHours" = "Время работы системы" "systemLoad" = "Нагрузка на систему" "systemLoadDesc" = "Средняя загрузка системы за последние 1, 5 и 15 минут" "connectionCount" = "Количество соединений" "ipAddresses" = "IP-адреса сервера" "toggleIpVisibility" = "Скрыть или показать IP-адреса сервера" "overallSpeed" = "Общая скорость передачи трафика" "upload" = "Отправка" "download" = "Загрузка" "totalData" = "Общий объем трафика" "sent" = "Отправлено" "received" = "Получено" "documentation" = "Документация" "xraySwitchVersionDialog" = "Переключить версию Xray" "xraySwitchVersionDialogDesc" = "Вы точно хотите сменить версию Xray?" "xraySwitchVersionPopover" = "Xray успешно обновлён" "geofileUpdateDialog" = "Вы действительно хотите обновить геофайл?" "geofileUpdateDialogDesc" = "Это обновит файл #filename#." "geofilesUpdateDialogDesc" = "Это обновит все геофайлы." "geofilesUpdateAll" = "Обновить все" "geofileUpdatePopover" = "Геофайлы успешно обновлены" "dontRefresh" = "Установка в процессе. Не обновляйте страницу" "logs" = "Журнал" "config" = "Конфигурация" "backup" = "Резервная копия" "backupTitle" = "Резервная копия базы данных" "exportDatabase" = "Экспорт базы данных" "exportDatabaseDesc" = "Нажмите, чтобы скачать файл .db, содержащий резервную копию вашей текущей базы данных на ваше устройство." "importDatabase" = "Импорт базы данных" "importDatabaseDesc" = "Нажмите, чтобы выбрать и загрузить файл .db с вашего устройства для восстановления базы данных из резервной копии." "importDatabaseSuccess" = "База данных успешно импортирована" "importDatabaseError" = "Произошла ошибка при импорте базы данных" "readDatabaseError" = "Произошла ошибка при чтении базы данных" "getDatabaseError" = "Произошла ошибка при получении базы данных" "getConfigError" = "Произошла ошибка при получении конфигурационного файла" [pages.inbounds] "allTimeTraffic" = "Общий трафик" "allTimeTrafficUsage" = "Общее использование за все время" "title" = "Подключения" "totalDownUp" = "Отправлено/получено" "totalUsage" = "Всего трафика" "inboundCount" = "Всего подключений" "operate" = "Меню" "enable" = "Включить" "remark" = "Примечание" "protocol" = "Протокол" "port" = "Порт" "portMap" = "Порт-маппинг" "traffic" = "Трафик" "details" = "Подробнее" "transportConfig" = "Транспорт" "expireDate" = "Дата окончания" "createdAt" = "Создано" "updatedAt" = "Обновлено" "resetTraffic" = "Сброс трафика" "addInbound" = "Создать подключение" "generalActions" = "Общие действия" "autoRefresh" = "Автообновление" "autoRefreshInterval" = "Интервал" "modifyInbound" = "Изменить подключение" "deleteInbound" = "Удалить подключение" "deleteInboundContent" = "Вы уверены, что хотите удалить подключение?" "deleteClient" = "Удалить клиента" "deleteClientContent" = "Вы уверены, что хотите удалить клиента?" "resetTrafficContent" = "Вы уверены, что хотите сбросить трафик?" "copyLink" = "Копировать ссылку" "address" = "Адрес" "network" = "Сеть" "destinationPort" = "Порт назначения" "targetAddress" = "Целевой адрес" "monitorDesc" = "Оставьте пустым для прослушивания всех IP-адресов" "meansNoLimit" = "= Без ограничений (значение: ГБ)" "totalFlow" = "Общий расход" "leaveBlankToNeverExpire" = "Оставьте пустым, чтобы было бесконечным" "noRecommendKeepDefault" = "Рекомендуется оставить настройки по умолчанию" "certificatePath" = "Путь к сертификату" "certificateContent" = "Содержимое сертификата" "publicKey" = "Публичный ключ" "privatekey" = "Приватный ключ" "clickOnQRcode" = "Нажмите на QR-код, чтобы скопировать" "client" = "Клиент" "export" = "Экспорт ссылок" "clone" = "Клонировать" "cloneInbound" = "Клонировать" "cloneInboundContent" = "Будут клонированы все настройки подключений, кроме списка клиентов, порта и IP-адреса прослушивания" "cloneInboundOk" = "Клонировано" "resetAllTraffic" = "Сброс трафика всех подключений" "resetAllTrafficTitle" = "Сброс трафика всех подключений" "resetAllTrafficContent" = "Вы уверены, что хотите сбросить трафик всех подключений?" "resetInboundClientTraffics" = "Сброс трафика клиента" "resetInboundClientTrafficTitle" = "Сброс трафика клиентов" "resetInboundClientTrafficContent" = "Вы уверены, что хотите сбросить трафик для этих клиентов?" "resetAllClientTraffics" = "Сброс трафика всех клиентов" "resetAllClientTrafficTitle" = "Сброс трафика всех клиентов" "resetAllClientTrafficContent" = "Вы уверены, что хотите сбросить трафик всех клиентов?" "delDepletedClients" = "Удалить отключенных клиентов" "delDepletedClientsTitle" = "Удаление отключенных клиентов" "delDepletedClientsContent" = "Вы уверены, что хотите удалить всех отключенных клиентов?" "email" = "Email" "emailDesc" = "Пожалуйста, укажите уникальный Email" "IPLimit" = "Лимит по количеству IP" "IPLimitDesc" = "Ограничение числа одновременных подключений с разных IP (0 – отключить)" "IPLimitlog" = "Лог IP-адресов" "IPLimitlogDesc" = "Лог IP-адресов (перед включением лога IP-адресов, вы должны очистить лог)" "IPLimitlogclear" = "Очистить лог" "setDefaultCert" = "Установить сертификат панели" "telegramDesc" = "Пожалуйста, укажите Chat ID Telegram. (используйте команду '/id' в боте) или (@userinfobot)" "subscriptionDesc" = "Вы можете найти свою ссылку подписки в разделе 'Подробнее'" "info" = "Информация" "same" = "Тот же" "inboundData" = "Данные подключений" "exportInbound" = "Экспорт подключений" "import" = "Импортировать" "importInbound" = "Импорт подключений" "periodicTrafficResetTitle" = "Сброс трафика" "periodicTrafficResetDesc" = "Автоматический сброс счетчика трафика через указанные интервалы" "lastReset" = "Последний сброс" [pages.client] "add" = "Добавить клиента" "edit" = "Редактировать клиента" "submitAdd" = "Добавить" "submitEdit" = "Сохранить изменения" "clientCount" = "Количество клиентов" "bulk" = "Добавить несколько" "method" = "Метод" "first" = "Первый" "last" = "Последний" "prefix" = "Префикс" "postfix" = "Постфикс" "delayedStart" = "Начало использования" "expireDays" = "Длительность" "days" = "дней" "renew" = "Автопродление" "renewDesc" = "Автопродление после истечения срока действия. (0 = отключить)(единица: день)" [pages.inbounds.periodicTrafficReset] "never" = "Никогда" "daily" = "Ежедневно" "weekly" = "Еженедельно" "monthly" = "Ежемесячно" [pages.inbounds.toasts] "obtain" = "Получить" "updateSuccess" = "Обновление прошло успешно" "logCleanSuccess" = "Лог был очищен" "inboundsUpdateSuccess" = "Подключения успешно обновлены" "inboundUpdateSuccess" = "Подключение успешно обновлено" "inboundCreateSuccess" = "Подключение успешно создано" "inboundDeleteSuccess" = "Подключение успешно удалено" "inboundClientAddSuccess" = "Клиент(ы) подключения добавлен(ы)" "inboundClientDeleteSuccess" = "Клиент подключения удалён" "inboundClientUpdateSuccess" = "Клиент подключения обновлён" "delDepletedClientsSuccess" = "Все исчерпанные клиенты удалены" "resetAllClientTrafficSuccess" = "Весь трафик клиента сброшен" "resetAllTrafficSuccess" = "Весь трафик сброшен" "resetInboundClientTrafficSuccess" = "Трафик сброшен" "trafficGetError" = "Ошибка получения данных о трафике" "getNewX25519CertError" = "Ошибка при получении сертификата X25519." "getNewmldsa65Error" = "Ошибка при получении сертификата mldsa65." "getNewVlessEncError" = "Ошибка при получении сертификата VlessEnc." [pages.inbounds.stream.general] "request" = "Запрос" "response" = "Ответ" "name" = "Имя" "value" = "Значение" [pages.inbounds.stream.tcp] "version" = "Версия" "method" = "Метод" "path" = "Путь" "status" = "Статус" "statusDescription" = "Описание статуса" "requestHeader" = "Заголовок запроса" "responseHeader" = "Заголовок ответа" [pages.settings] "title" = "Настройки" "save" = "Сохранить" "infoDesc" = "Сохраните изменения и перезапустите панель для их применения." "restartPanel" = "Перезапуск панели" "restartPanelDesc" = "Вы уверены, что хотите перезапустить панель? Подтвердите, и перезапуск произойдёт через 3 секунды. Если панель будет недоступна, проверьте лог сервера" "restartPanelSuccess" = "Панель успешно перезапущена" "actions" = "Действия" "resetDefaultConfig" = "Восстановить настройки по умолчанию" "panelSettings" = "Панель" "securitySettings" = "Учетная запись" "TGBotSettings" = "Telegram-Бот" "panelListeningIP" = "IP-адрес для управления панелью" "panelListeningIPDesc" = "Оставьте пустым для подключения с любого IP" "panelListeningDomain" = "Домен панели" "panelListeningDomainDesc" = "Оставьте пустым для подключения с любых доменов и IP." "panelPort" = "Порт панели" "panelPortDesc" = "Порт, на котором работает панель" "publicKeyPath" = "Путь к файлу публичного ключа сертификата панели" "publicKeyPathDesc" = "Введите полный путь, начинающийся с '/'" "privateKeyPath" = "Путь к файлу приватного ключа сертификата панели" "privateKeyPathDesc" = "Введите полный путь, начинающийся с '/'" "panelUrlPath" = "Корневой путь URL адреса панели" "panelUrlPathDesc" = "Должен начинаться с '/' и заканчиваться '/'" "pageSize" = "Размер нумерации страниц" "pageSizeDesc" = "Определить размер страницы для таблицы подключений. Установите 0, чтобы отключить" "remarkModel" = "Модель примечания и символ разделения" "datepicker" = "Тип календаря" "datepickerPlaceholder" = "Выберите дату" "datepickerDescription" = "Запланированные задачи будут выполняться в соответствии с этим календарем." "sampleRemark" = "Пример примечания" "oldUsername" = "Текущий логин" "currentPassword" = "Текущий пароль" "newUsername" = "Новый логин" "newPassword" = "Новый пароль" "telegramBotEnable" = "Включить Telegram бота" "telegramBotEnableDesc" = "Доступ к функциям панели через Telegram-бота" "telegramToken" = "Токен Telegram бота" "telegramTokenDesc" = "Необходимо получить токен у менеджера ботов Telegram @botfather" "telegramProxy" = "Прокси-сервер Socks5" "telegramProxyDesc" = "Если для подключения к Telegram вам нужен прокси Socks5, настройте его параметры согласно руководству." "telegramAPIServer" = "API-сервер Telegram" "telegramAPIServerDesc" = "Используемый API-сервер Telegram. Оставьте пустым, чтобы использовать сервер по умолчанию." "telegramChatId" = "User ID администратора бота" "telegramChatIdDesc" = "Один или несколько User ID администратора(-ов) Telegram-бота. Для получения User ID используйте @userinfobot или команду '/id' в боте." "telegramNotifyTime" = "Частота уведомлений для администраторов от бота" "telegramNotifyTimeDesc" = "Укажите интервал уведомлений в формате Crontab" "tgNotifyBackup" = "Резервное копирование базы данных" "tgNotifyBackupDesc" = "Отправлять уведомление с файлом резервной копии базы данных" "tgNotifyLogin" = "Уведомление о входе" "tgNotifyLoginDesc" = "Отображает имя пользователя, IP-адрес и время, когда кто-то пытается войти в вашу панель." "sessionMaxAge" = "Продолжительность сессии" "sessionMaxAgeDesc" = "Продолжительность сессии в системе (значение: минута)" "expireTimeDiff" = "Задержка уведомления об истечении сессии" "expireTimeDiffDesc" = "Получение уведомления об истечении срока действия сессии до достижения порогового значения (значение: день)" "trafficDiff" = "Порог трафика для уведомления" "trafficDiffDesc" = "Получение уведомления об исчерпании трафика до достижения порога (значение: ГБ)" "tgNotifyCpu" = "Порог нагрузки на ЦП для уведомления" "tgNotifyCpuDesc" = "Уведомление администраторов в Telegram, если нагрузка на ЦП превышает этот порог (значение: %)" "timeZone" = "Часовой пояс" "timeZoneDesc" = "Запланированные задачи выполняются в соответствии со временем в этом часовом поясе" "subSettings" = "Подписка" "subEnable" = "Включить подписку" "subEnableDesc" = "Функция подписки с отдельной конфигурацией" "subJsonEnable" = "Включить/отключить JSON-эндпоинт подписки независимо." "subTitle" = "Заголовок подписки" "subTitleDesc" = "Название подписки, которое видит клиент в VPN-клиенте" "subSupportUrl" = "URL поддержки" "subSupportUrlDesc" = "Ссылка на техническую поддержку, отображаемая в VPN-клиенте" "subProfileUrl" = "URL профиля" "subProfileUrlDesc" = "Ссылка на ваш сайт, отображаемая в VPN-клиенте" "subAnnounce" = "Объявление" "subAnnounceDesc" = "Текст объявления, отображаемый в VPN-клиенте" "subEnableRouting" = "Включить маршрутизацию" "subEnableRoutingDesc" = "Глобальная настройка для включения маршрутизации в VPN-клиенте. (Только для Happ)" "subRoutingRules" = "Правила маршрутизации" "subRoutingRulesDesc" = "Глобальные правила маршрутизации для VPN-клиента. (Только для Happ)" "subListen" = "Прослушивание IP" "subListenDesc" = "Оставьте пустым по умолчанию, чтобы отслеживать все IP-адреса" "subPort" = "Порт подписки" "subPortDesc" = "Номер порта для обслуживания службы подписки не должен использоваться на сервере" "subCertPath" = "Путь к файлу публичного ключа сертификата подписки" "subCertPathDesc" = "Введите полный путь, начинающийся с '/'" "subKeyPath" = "Путь к файлу приватного ключа сертификата подписки" "subKeyPathDesc" = "Введите полный путь, начинающийся с '/'" "subPath" = "Корневой путь URL-адреса подписки" "subPathDesc" = "Должен начинаться с '/' и заканчиваться на '/'" "subDomain" = "Домен прослушивания" "subDomainDesc" = "Оставьте пустым по умолчанию, чтобы слушать все домены и IP-адреса" "subUpdates" = "Интервалы обновления подписки" "subUpdatesDesc" = "Интервал между обновлениями в клиентском приложении (в часах)" "subEncrypt" = "Шифровать конфиги" "subEncryptDesc" = "Шифровать возвращенные конфиги в подписке" "subShowInfo" = "Показать информацию об использовании" "subShowInfoDesc" = "Отображать остаток трафика и дату окончания после имени конфигурации" "subURI" = "URI обратного прокси" "subURIDesc" = "Изменить базовый URI URL-адреса подписки для использования за прокси-серверами" "externalTrafficInformEnable" = "Информация о внешнем трафике" "externalTrafficInformEnableDesc" = "Информировать внешний API о каждом обновлении трафика" "externalTrafficInformURI" = "URI информации о внешнем трафике" "externalTrafficInformURIDesc" = "Обновления трафика отправляются на этот URI" "fragment" = "Фрагментация" "fragmentDesc" = "Включить фрагментацию TLS-хэндшейка" "fragmentSett" = "Настройки фрагментации" "noisesDesc" = "Включить Noises." "noisesSett" = "Настройки Noises" "mux" = "Mux" "muxDesc" = "Передача нескольких независимых потоков данных в одном соединении." "muxSett" = "Настройки Mux" "direct" = "Прямое подключение" "directDesc" = "Устанавливает прямые соединения с доменами или IP-адресами определённой страны." "notifications" = "Уведомления" "certs" = "Сертификаты" "externalTraffic" = "Внешний трафик" "dateAndTime" = "Дата и время" "proxyAndServer" = "Прокси и сервер" "intervals" = "Интервалы" "information" = "Информация" "language" = "Язык интерфейса" "telegramBotLanguage" = "Язык Telegram-бота" [pages.xray] "title" = "Настройки Xray" "save" = "Сохранить" "restart" = "Перезапуск Xray" "restartSuccess" = "Xray успешно перезапущен" "stopSuccess" = "Xray успешно остановлен" "restartError" = "Произошла ошибка при перезапуске Xray." "stopError" = "Произошла ошибка при остановке Xray." "basicTemplate" = "Основное" "advancedTemplate" = "Расширенный шаблон" "generalConfigs" = "Основные настройки" "generalConfigsDesc" = "Эти параметры описывают общие настройки" "logConfigs" = "Логи" "logConfigsDesc" = "Логи могут замедлять работу сервера. Включайте только нужные вам виды логов при необходимости!" "blockConfigsDesc" = "Настройте, чтобы клиенты не имели доступа к определенным протоколам" "basicRouting" = "Базовые соединения" "blockConnectionsConfigsDesc" = "Эти параметры будут блокировать трафик в зависимости от страны назначения." "directConnectionsConfigsDesc" = "Прямое соединение означает, что определенный трафик не будет перенаправлен через другой сервер." "blockips" = "Заблокированные IP-адреса" "blockdomains" = "Заблокированные домены" "directips" = "Прямые IP-адреса" "directdomains" = "Прямые домены" "ipv4Routing" = "Правила IPv4" "ipv4RoutingDesc" = "Эти параметры позволят клиентам маршрутизироваться к целевым доменам только через IPv4" "warpRouting" = "Правила WARP" "warpRoutingDesc" = " Эти опции будут направлять трафик в зависимости от конкретного пункта назначения через WARP." "Template" = "Шаблон конфигурации Xray" "TemplateDesc" = "На основе шаблона создаётся конфигурационный файл Xray." "FreedomStrategy" = "Настройка стратегии протокола Freedom" "FreedomStrategyDesc" = "Установка стратегии вывода сети в протоколе Freedom" "RoutingStrategy" = "Настройка маршрутизации доменов" "RoutingStrategyDesc" = "Установка общей стратегии маршрутизации разрешения DNS" "outboundTestUrl" = "URL для теста исходящего" "outboundTestUrlDesc" = "URL для проверки подключения исходящего" "Torrent" = "Заблокировать BitTorrent" "Inbounds" = "Входящие подключения" "InboundsDesc" = "Изменение шаблона конфигурации для подключения определенных клиентов" "Outbounds" = "Исходящие подключения" "Balancers" = "Балансировщик" "OutboundsDesc" = "Изменение шаблона конфигурации, чтобы определить исходящие подключения для этого сервера" "Routings" = "Маршрутизация" "RoutingsDesc" = "Важен приоритет каждого правила!" "completeTemplate" = "Все" "logLevel" = "Уровень логов" "logLevelDesc" = "Уровень журнала для журналов ошибок, указывающий информацию, которую необходимо записать." "accessLog" = "Логи доступа" "accessLogDesc" = "Путь к файлу журнала доступа. Специальное значение «none» отключает логи доступа." "errorLog" = "Логи ошибок" "errorLogDesc" = "Путь к файлу логов ошибок. Специальное значение «none» отключает логи ошибок." "dnsLog" = "Логи DNS" "dnsLogDesc" = "Включить логи запросов DNS" "maskAddress" = "Маскировка адреса" "maskAddressDesc" = "При активации реальный IP-адрес заменяется на маскировочный в логах." "statistics" = "Статистика" "statsInboundUplink" = "Статистика входящего аплинка" "statsInboundUplinkDesc" = "Включает сбор статистики для исходящего трафика всех входящих прокси." "statsInboundDownlink" = "Статистика входящего даунлинка" "statsInboundDownlinkDesc" = "Включает сбор статистики для входящего трафика всех входящих прокси." "statsOutboundUplink" = "Статистика исходящего аплинка" "statsOutboundUplinkDesc" = "Включает сбор статистики для исходящего трафика всех исходящих прокси." "statsOutboundDownlink" = "Статистика исходящего даунлинка" "statsOutboundDownlinkDesc" = "Включает сбор статистики для входящего трафика всех исходящих прокси." [pages.xray.rules] "first" = "Первый" "last" = "Последний" "up" = "Поднять вверх" "down" = "Опустить вниз" "source" = "Источник" "dest" = "Пункт назначения" "inbound" = "Входящее подключение" "outbound" = "Исходящее подключение" "balancer" = "Балансировщик" "info" = "Информация" "add" = "Создать правило" "edit" = "Редактировать правило" "useComma" = "Элементы, разделённые запятыми" [pages.xray.outbound] "addOutbound" = "Создать исходящее подключение" "addReverse" = "Создать реверс-прокси" "editOutbound" = "Изменить исходящее подключение" "editReverse" = "Редактировать реверс-прокси" "tag" = "Тег" "tagDesc" = "Уникальный тег" "address" = "Адрес" "reverse" = "Реверс-прокси" "domain" = "Домен" "type" = "Тип" "bridge" = "Мост" "portal" = "Портал" "link" = "Ссылка" "intercon" = "Соединение" "settings" = "Настройки" "accountInfo" = "Информация об учетной записи" "outboundStatus" = "Статус исходящего подключения" "sendThrough" = "Отправить через" "test" = "Тест" "testResult" = "Результат теста" "testing" = "Тестирование соединения..." "testSuccess" = "Тест успешен" "testFailed" = "Тест не пройден" "testError" = "Не удалось протестировать исходящее подключение" [pages.xray.balancer] "addBalancer" = "Создать балансировщик" "editBalancer" = "Редактировать балансировщик" "balancerStrategy" = "Стратегия" "balancerSelectors" = "Селекторы" "tag" = "Тег" "tagDesc" = "Уникальный тег" "balancerDesc" = "Невозможно одновременно использовать balancerTag и outboundTag. При одновременном использовании будет работать только outboundTag." [pages.xray.wireguard] "secretKey" = "Секретный ключ" "publicKey" = "Публичный ключ" "allowedIPs" = "Разрешенные IP-адреса" "endpoint" = "Конечная точка" "psk" = "Общий ключ" "domainStrategy" = "Стратегия домена" [pages.xray.tun] "nameDesc" = "Имя интерфейса TUN. Значение по умолчанию - 'xray0'" "mtuDesc" = "Максимальная единица передачи. Максимальный размер пакетов данных. Значение по умолчанию - 1500" "userLevel" = "Уровень пользователя" "userLevelDesc" = "Все соединения, установленные через этот входящий поток, будут использовать этот уровень пользователя. Значение по умолчанию - 0" [pages.xray.dns] "enable" = "Включить DNS" "enableDesc" = "Включить встроенный DNS-сервер" "tag" = "Название тега DNS" "tagDesc" = "Этот тег будет доступен как входящий тег в правилах маршрутизации." "clientIp" = "IP клиента" "clientIpDesc" = "Используется для уведомления сервера о указанном местоположении IP во время DNS-запросов" "disableCache" = "Отключить кэш" "disableCacheDesc" = "Отключает кэширование DNS" "disableFallback" = "Отключить резервный DNS" "disableFallbackDesc" = "Отключает резервные DNS-запросы" "disableFallbackIfMatch" = "Отключить резервный DNS при совпадении" "disableFallbackIfMatchDesc" = "Отключает резервные DNS-запросы при совпадении списка доменов DNS-сервера" "enableParallelQuery" = "Включить параллельные запросы" "enableParallelQueryDesc" = "Включить параллельные DNS-запросы к нескольким серверам для более быстрого разрешения" "strategy" = "Стратегия запроса" "strategyDesc" = "Общая стратегия разрешения доменных имен" "add" = "Создать DNS" "edit" = "Редактировать DNS" "domains" = "Домены" "expectIPs" = "Ожидаемые IP" "unexpectIPs" = "Неожидаемые IP" "useSystemHosts" = "Использовать системные Hosts" "useSystemHostsDesc" = "Использовать файл hosts из установленной системы" "usePreset" = "Использовать шаблон" "dnsPresetTitle" = "Шаблоны DNS" "dnsPresetFamily" = "Семейный" [pages.xray.fakedns] "add" = "Создать Fake DNS" "edit" = "Редактировать Fake DNS" "ipPool" = "Подсеть пула IP" "poolSize" = "Размер пула" [pages.settings.security] "admin" = "Учетные данные администратора" "twoFactor" = "Двухфакторная аутентификация" "twoFactorEnable" = "Включить 2FA" "twoFactorEnableDesc" = "Добавляет дополнительный уровень аутентификации для повышения безопасности." "twoFactorModalSetTitle" = "Включить двухфакторную аутентификацию" "twoFactorModalDeleteTitle" = "Отключить двухфакторную аутентификацию" "twoFactorModalSteps" = "Для настройки двухфакторной аутентификации выполните несколько шагов:" "twoFactorModalFirstStep" = "1. Отсканируйте этот QR-код в приложении для аутентификации или скопируйте токен рядом с QR-кодом и вставьте его в приложение" "twoFactorModalSecondStep" = "2. Введите код из приложения" "twoFactorModalRemoveStep" = "Введите код из приложения, чтобы отключить двухфакторную аутентификацию." "twoFactorModalChangeCredentialsTitle" = "Изменить учетные данные" "twoFactorModalChangeCredentialsStep" = "Введите код из приложения, чтобы изменить учетные данные администратора." "twoFactorModalSetSuccess" = "Двухфакторная аутентификация была успешно установлена" "twoFactorModalDeleteSuccess" = "Двухфакторная аутентификация была успешно удалена" "twoFactorModalError" = "Неверный код" [pages.settings.toasts] "modifySettings" = "Настройки изменены" "getSettings" = "Произошла ошибка при получении параметров." "modifyUserError" = "Произошла ошибка при изменении учетных данных администратора." "modifyUser" = "Вы успешно изменили учетные данные администратора." "originalUserPassIncorrect" = "Неверное имя пользователя или пароль" "userPassMustBeNotEmpty" = "Новое имя пользователя и новый пароль должны быть заполнены" "getOutboundTrafficError" = "Ошибка получения трафика исходящего подключения" "resetOutboundTrafficError" = "Ошибка сброса трафика исходящего подключения" [tgbot] "keyboardClosed" = "❌ Клавиатура закрыта." "noResult" = "❗ Нет результатов." "noQuery" = "❌ Запрос не найден. Пожалуйста, повторите команду." "wentWrong" = "❌ Что-то пошло не так..." "noIpRecord" = "❗ Нет записей об IP-адресе." "noInbounds" = "❗ У вас не настроено ни одного входящего подключения." "unlimited" = "♾ Безлимит" "add" = "Добавить" "month" = "Месяц" "months" = "Месяцев" "day" = "День" "days" = "Дней" "hours" = "Часов" "minutes" = "Минуты" "unknown" = "Неизвестно" "inbounds" = "Входящие подключения" "clients" = "Клиенты" "offline" = "🔴 Офлайн" "online" = "🟢 Онлайн" [tgbot.commands] "unknown" = "❗ Неизвестная команда" "pleaseChoose" = "👇 Пожалуйста, выберите:\r\n" "help" = "🤖 Добро пожаловать! Этот бот предназначен для предоставления вам данных с сервера и позволяет вносить изменения на него.\r\n\r\n" "start" = "👋 Привет, {{ .Firstname }}.\r\n" "welcome" = "🤖 Добро пожаловать в бота управления {{ .Hostname }}!\r\n" "status" = "✅ Бот функционирует нормально." "usage" = "❗ Пожалуйста, укажите email для поиска." "getID" = "🆔 Ваш User ID: {{ .ID }}" "helpAdminCommands" = "🔃 Для перезапуска Xray Core:\r\n/restart\r\n\r\n🔎 Для поиска клиента по email:\r\n/usage [Email]\r\n\r\n📊 Для поиска входящих подключений (со статистикой клиентов):\r\n/inbound [имя подключения]\r\n\r\n🆔 Ваш Telegram User ID:\r\n/id" "helpClientCommands" = "💲 Для просмотра информации о вашей подписке используйте команду:\r\n/usage [Email]\r\n\r\n🆔 Ваш Telegram User ID:\r\n/id" "restartUsage" = "\r\n\r\n/restart" "restartSuccess" = "✅ Ядро Xray успешно перезапущено." "restartFailed" = "❗ Ошибка при перезапуске Xray-core.\r\n\r\nОшибка: {{ .Error }}." "xrayNotRunning" = "❗ Xray Core не запущен." "startDesc" = "Показать главное меню" "helpDesc" = "Справка по боту" "statusDesc" = "Проверить статус бота" "idDesc" = "Показать ваш Telegram ID" [tgbot.messages] "cpuThreshold" = "🔴 Загрузка процессора составляет {{ .Percent }}%, что превышает пороговое значение {{ .Threshold }}%" "selectUserFailed" = "❌ Ошибка при выборе пользователя." "userSaved" = "✅ Пользователь Telegram сохранен." "loginSuccess" = "✅ Успешный вход в панель.\r\n" "loginFailed" = "❗️ Ошибка входа в панель.\r\n" "2faFailed" = "Ошибка 2FA" "report" = "🕰 Запланированные отчеты: {{ .RunTime }}\r\n" "datetime" = "⏰ Дата и время: {{ .DateTime }}\r\n" "hostname" = "💻 Имя хоста: {{ .Hostname }}\r\n" "version" = "🚀 Версия X-UI: {{ .Version }}\r\n" "xrayVersion" = "📡 Версия Xray: {{ .XrayVersion }}\r\n" "ipv6" = "🌐 IPv6: {{ .IPv6 }}\r\n" "ipv4" = "🌐 IPv4: {{ .IPv4 }}\r\n" "ip" = "🌐 IP: {{ .IP }}\r\n" "ips" = "🔢 IP-адреса:\r\n{{ .IPs }}\r\n" "serverUpTime" = "⏳ Время работы сервера: {{ .UpTime }} {{ .Unit }}\r\n" "serverLoad" = "📈 Нагрузка сервера: {{ .Load1 }}, {{ .Load2 }}, {{ .Load3 }}\r\n" "serverMemory" = "📋 ОЗУ сервера: {{ .Current }}/{{ .Total }}\r\n" "tcpCount" = "🔹 Количество TCP-соединений: {{ .Count }}\r\n" "udpCount" = "🔸 Количество UDP-соединений: {{ .Count }}\r\n" "traffic" = "🚦 Трафик: {{ .Total }} (↑{{ .Upload }},↓{{ .Download }})\r\n" "xrayStatus" = "ℹ️ Состояние Xray: {{ .State }}\r\n" "username" = "👤 Имя пользователя: {{ .Username }}\r\n" "password" = "👤 Пароль: {{ .Password }}\r\n" "time" = "⏰ Время: {{ .Time }}\r\n" "inbound" = "📍 Входящее подключение: {{ .Remark }}\r\n" "port" = "🔌 Порт: {{ .Port }}\r\n" "expire" = "📅 Дата окончания: {{ .Time }}\r\n" "expireIn" = "📅 Окончание через: {{ .Time }}\r\n" "active" = "💡 Активен: {{ .Enable }}\r\n" "enabled" = "🚨 Активен: {{ .Enable }}\r\n" "online" = "🌐 Статус соединения: {{ .Status }}\r\n" "lastOnline" = "🔙 Был(а) в сети: {{ .Time }}\r\n" "email" = "📧 Email: {{ .Email }}\r\n" "upload" = "🔼 Исходящий трафик: ↑{{ .Upload }}\r\n" "download" = "🔽 Входящий трафик: ↓{{ .Download }}\r\n" "total" = "📊 Всего: ↑↓{{ .UpDown }} из {{ .Total }}\r\n" "TGUser" = "👤 Telegram User ID: {{ .TelegramID }}\r\n" "exhaustedMsg" = "🚨 Исчерпаны {{ .Type }}:\r\n" "exhaustedCount" = "🚨 Количество исчерпанных {{ .Type }}:\r\n" "onlinesCount" = "🌐 Клиентов онлайн: {{ .Count }}\r\n" "disabled" = "🛑 Отключено: {{ .Disabled }}\r\n" "depleteSoon" = "🔜 Клиенты, у которых скоро исчерпание: {{ .Deplete }}\r\n\r\n" "backupTime" = "🗄 Время резервного копирования: {{ .Time }}\r\n" "refreshedOn" = "\r\n📋🔄 Обновлено: {{ .Time }}\r\n\r\n" "yes" = "✅ Да" "no" = "❌ Нет" "received_id" = "🔑📥 ID обновлён." "received_password" = "🔑📥 Пароль обновлён." "received_email" = "📧📥 Email обновлен." "received_comment" = "💬📥 Комментарий обновлён." "id_prompt" = "🔑 Стандартный ID: {{ .ClientId }}\n\nВведите ваш ID." "pass_prompt" = "🔑 Стандартный пароль: {{ .ClientPassword }}\n\nВведите ваш пароль." "email_prompt" = "📧 Стандартный email: {{ .ClientEmail }}\n\nВведите ваш email." "comment_prompt" = "💬 Стандартный комментарий: {{ .ClientComment }}\n\nВведите ваш комментарий." "inbound_client_data_id" = "🔄 Входящие подключения: {{ .InboundRemark }}\n\n🔑 ID: {{ .ClientId }}\n📧 Email: {{ .ClientEmail }}\n📊 Трафик: {{ .ClientTraffic }}\n📅 Срок действия: {{ .ClientExp }}\n💬 Комментарий: {{ .ClientComment }}\n\nТеперь вы можете добавить клиента в входящее подключение!" "inbound_client_data_pass" = "🔄 Входящие подключения: {{ .InboundRemark }}\n\n🔑 Пароль: {{ .ClientPass }}\n📧 Email: {{ .ClientEmail }}\n📊 Трафик: {{ .ClientTraffic }}\n📅 Срок действия: {{ .ClientExp }}\n💬 Комментарий: {{ .ClientComment }}\n\nТеперь вы можете добавить клиента в входящее подключение!" "cancel" = "❌ Процесс отменён! \n\nВы можете снова начать с /start в любое время. 🔄" "error_add_client" = "⚠️ Ошибка:\n\n {{ .error }}" "using_default_value" = "Используется значение по умолчанию👌" "incorrect_input" = "Ваш ввод недействителен.\nФразы должны быть непрерывными без пробелов.\nПравильный пример: aaaaaa\nНеправильный пример: aaa aaa 🚫" "AreYouSure" = "Вы уверены? 🤔" "SuccessResetTraffic" = "📧 Почта: {{ .ClientEmail }}\n🏁 Результат: ✅ Успешно" "FailedResetTraffic" = "📧 Почта: {{ .ClientEmail }}\n🏁 Результат: ❌ Неудача \n\n🛠️ Ошибка: [ {{ .ErrorMessage }} ]" "FinishProcess" = "🔚 Сброс трафика завершён для всех клиентов." [tgbot.buttons] "closeKeyboard" = "❌ Закрыть клавиатуру" "cancel" = "❌ Отмена" "cancelReset" = "❌ Отменить сброс" "cancelIpLimit" = "❌ Отменить лимит IP" "confirmResetTraffic" = "✅ Подтвердить сброс трафика?" "confirmClearIps" = "✅ Подтвердить очистку IP?" "confirmRemoveTGUser" = "✅ Подтвердить удаление пользователя Telegram?" "confirmToggle" = "✅ Подтвердить вкл/выкл пользователя?" "dbBackup" = "📂 Бэкап БД" "serverUsage" = "💻 Состояние сервера" "getInbounds" = "🔌 Входящие подключения" "depleteSoon" = "⚠️ Скоро конец" "clientUsage" = "Статистика клиента" "onlines" = "🟢 Онлайн" "commands" = "🖱️ Команды" "refresh" = "🔄 Обновить" "clearIPs" = "❌ Очистить IP" "removeTGUser" = "❌ Удалить пользователя Telegram" "selectTGUser" = "👤 Выбрать пользователя Telegram" "selectOneTGUser" = "👤 Выберите пользователя Telegram:" "resetTraffic" = "📈 Сбросить трафик" "resetExpire" = "📅 Изменить дату окончания" "ipLog" = "🔢 Лог IP" "ipLimit" = "🔢 Лимит IP" "setTGUser" = "👤 Установить пользователя Telegram" "toggle" = "🔘 Вкл./Выкл." "custom" = "🔢 Свой" "confirmNumber" = "✅ Подтвердить: {{ .Num }}" "confirmNumberAdd" = "✅ Подтвердить добавление: {{ .Num }}" "limitTraffic" = "🚧 Лимит трафика" "getBanLogs" = "📄 Лог банов" "allClients" = "👥 Все клиенты" "addClient" = "➕ Новый клиент" "submitDisable" = "Добавить отключенным ☑️" "submitEnable" = "Добавить включенным ✅" "use_default" = "🏷️ Использовать по умолчанию" "change_id" = "⚙️🔑 ID" "change_password" = "⚙️🔑 Пароль" "change_email" = "⚙️📧 Email" "change_comment" = "⚙️💬 Комментарий" "ResetAllTraffics" = "Сбросить весь трафик" "SortedTrafficUsageReport" = "Отсортированный отчет об использовании трафика" [tgbot.answers] "successfulOperation" = "✅ Успешно!" "errorOperation" = "❗ Ошибка в операции." "getInboundsFailed" = "❌ Не удалось получить входящие подключения." "getClientsFailed" = "❌ Не удалось получить клиентов." "canceled" = "❌ {{ .Email }}: Операция отменена." "clientRefreshSuccess" = "✅ {{ .Email }}: Клиент успешно обновлен." "IpRefreshSuccess" = "✅ {{ .Email }}: IP-адреса успешно обновлены." "TGIdRefreshSuccess" = "✅ {{ .Email }}: Пользователь Telegram клиента успешно обновлен." "resetTrafficSuccess" = "✅ {{ .Email }}: Трафик успешно сброшен." "setTrafficLimitSuccess" = "✅ {{ .Email }}: Лимит трафика успешно установлен." "expireResetSuccess" = "✅ {{ .Email }}: Срок действия успешно сброшен." "resetIpSuccess" = "✅ {{ .Email }}: Лимит IP ({{ .Count }}) успешно сохранен." "clearIpSuccess" = "✅ {{ .Email }}: IP-адреса успешно очищены." "getIpLog" = "✅ {{ .Email }}: Получен лог IP." "getUserInfo" = "✅ {{ .Email }}: Получена информация о пользователе Telegram." "removedTGUserSuccess" = "✅ {{ .Email }}: Пользователь Telegram успешно удален." "enableSuccess" = "✅ {{ .Email }}: Включено успешно." "disableSuccess" = "✅ {{ .Email }}: Отключено успешно." "askToAddUserId" = "❌ Ваша конфигурация не найдена!\r\n💭 Пожалуйста, попросите администратора использовать ваш Telegram User ID в конфигурации.\r\n\r\n🆔 Ваш User ID: {{ .TgUserID }}" "chooseClient" = "Выберите клиента для входящего подключения {{ .Inbound }}" "chooseInbound" = "Выберите входящее подключение" ================================================ FILE: web/translation/translate.tr_TR.toml ================================================ "username" = "Kullanıcı Adı" "password" = "Şifre" "login" = "Giriş Yap" "confirm" = "Onayla" "cancel" = "İptal" "close" = "Kapat" "create" = "Oluştur" "update" = "Güncelle" "copy" = "Kopyala" "copied" = "Kopyalandı" "download" = "İndir" "remark" = "Açıklama" "enable" = "Etkin" "protocol" = "Protokol" "search" = "Ara" "filter" = "Filtrele" "loading" = "Yükleniyor..." "second" = "Saniye" "minute" = "Dakika" "hour" = "Saat" "day" = "Gün" "check" = "Kontrol Et" "indefinite" = "Belirsiz" "unlimited" = "Sınırsız" "none" = "Hiçbiri" "qrCode" = "QR Kod" "info" = "Daha Fazla Bilgi" "edit" = "Düzenle" "delete" = "Sil" "reset" = "Sıfırla" "noData" = "Veri yok." "copySuccess" = "Başarıyla Kopyalandı" "sure" = "Emin misiniz" "encryption" = "Şifreleme" "useIPv4ForHost" = "Ana bilgisayar için IPv4 kullan" "transmission" = "İletim" "host" = "Sunucu" "path" = "Yol" "camouflage" = "Kandırma" "status" = "Durum" "enabled" = "Etkin" "disabled" = "Devre Dışı" "depleted" = "Bitti" "depletingSoon" = "Bitmek Üzere" "offline" = "Çevrimdışı" "online" = "Çevrimiçi" "domainName" = "Alan Adı" "monitor" = "Dinleme IP" "certificate" = "Dijital Sertifika" "fail" = "Başarısız" "comment" = "Yorum" "success" = "Başarılı" "lastOnline" = "Son çevrimiçi" "getVersion" = "Sürümü Al" "install" = "Yükle" "clients" = "Müşteriler" "usage" = "Kullanım" "twoFactorCode" = "Kod" "remained" = "Kalan" "security" = "Güvenlik" "secAlertTitle" = "Güvenlik Uyarısı" "secAlertSsl" = "Bu bağlantı güvenli değil. Verilerin korunması için TLS etkinleştirilene kadar hassas bilgiler girmekten kaçının." "secAlertConf" = "Bazı ayarlar saldırılara açıktır. Olası ihlalleri önlemek için güvenlik protokollerini güçlendirmeniz önerilir." "secAlertSSL" = "Panelde güvenli bağlantı yok. Verilerin korunması için TLS sertifikası yükleyin." "secAlertPanelPort" = "Panel varsayılan portu savunmasız. Rastgele veya belirli bir port yapılandırın." "secAlertPanelURI" = "Panel varsayılan URI yolu güvensiz. Karmaşık bir URI yolu yapılandırın." "secAlertSubURI" = "Abonelik varsayılan URI yolu güvensiz. Karmaşık bir URI yolu yapılandırın." "secAlertSubJsonURI" = "Abonelik JSON varsayılan URI yolu güvensiz. Karmaşık bir URI yolu yapılandırın." "emptyDnsDesc" = "Eklenmiş DNS sunucusu yok." "emptyFakeDnsDesc" = "Eklenmiş Fake DNS sunucusu yok." "emptyBalancersDesc" = "Eklenmiş dengeleyici yok." "emptyReverseDesc" = "Eklenmiş ters proxy yok." "somethingWentWrong" = "Bir şeyler yanlış gitti" [subscription] "title" = "Abonelik Bilgisi" "subId" = "Abonelik Kimliği" "status" = "Durum" "downloaded" = "İndirilen" "uploaded" = "Yüklenen" "expiry" = "Son Kullanma" "totalQuota" = "Toplam Kota" "individualLinks" = "Bireysel Bağlantılar" "active" = "Aktif" "inactive" = "Pasif" "unlimited" = "Sınırsız" "noExpiry" = "Süresiz" [menu] "theme" = "Tema" "dark" = "Koyu" "ultraDark" = "Ultra Koyu" "dashboard" = "Genel Bakış" "inbounds" = "Gelenler" "settings" = "Panel Ayarları" "xray" = "Xray Yapılandırmaları" "logout" = "Çıkış Yap" "link" = "Yönet" [pages.login] "hello" = "Merhaba" "title" = "Hoş Geldiniz" "loginAgain" = "Oturum süreniz doldu, lütfen tekrar giriş yapın" [pages.login.toasts] "invalidFormData" = "Girdi verisi formatı geçersiz." "emptyUsername" = "Kullanıcı adı gerekli" "emptyPassword" = "Şifre gerekli" "wrongUsernameOrPassword" = "Geçersiz kullanıcı adı, şifre veya iki adımlı doğrulama kodu." "successLogin" = "Hesabınıza başarıyla giriş yaptınız." [pages.index] "title" = "Genel Bakış" "cpu" = "İşlemci" "logicalProcessors" = "Mantıksal işlemciler" "frequency" = "Frekans" "swap" = "Takas" "storage" = "Depolama" "memory" = "RAM" "threads" = "İş parçacıkları" "xrayStatus" = "Xray" "stopXray" = "Durdur" "restartXray" = "Yeniden Başlat" "xraySwitch" = "Sürüm" "xraySwitchClick" = "Geçiş yapmak istediğiniz sürümü seçin." "xraySwitchClickDesk" = "Dikkatli seçin, eski sürümler mevcut yapılandırmalarla uyumlu olmayabilir." "xrayStatusUnknown" = "Bilinmiyor" "xrayStatusRunning" = "Çalışıyor" "xrayStatusStop" = "Durduruldu" "xrayStatusError" = "Hata" "xrayErrorPopoverTitle" = "Xray çalıştırılırken bir hata oluştu" "operationHours" = "Çalışma Süresi" "systemLoad" = "Sistem Yükü" "systemLoadDesc" = "Geçmiş 1, 5 ve 15 dakika için sistem yük ortalaması" "connectionCount" = "Bağlantı İstatistikleri" "ipAddresses" = "IP adresleri" "toggleIpVisibility" = "IP görünürlüğünü değiştir" "overallSpeed" = "Genel hız" "upload" = "Yükleme" "download" = "İndirme" "totalData" = "Toplam veri" "sent" = "Gönderilen" "received" = "Alınan" "documentation" = "Dokümantasyon" "xraySwitchVersionDialog" = "Xray sürümünü gerçekten değiştirmek istiyor musunuz?" "xraySwitchVersionDialogDesc" = "Bu işlem Xray sürümünü #version# olarak değiştirecektir." "xraySwitchVersionPopover" = "Xray başarıyla güncellendi" "geofileUpdateDialog" = "Geofile'ı gerçekten güncellemek istiyor musunuz?" "geofileUpdateDialogDesc" = "Bu işlem #filename# dosyasını güncelleyecektir." "geofilesUpdateDialogDesc" = "Bu, tüm dosyaları güncelleyecektir." "geofilesUpdateAll" = "Tümünü güncelle" "geofileUpdatePopover" = "Geofile başarıyla güncellendi" "dontRefresh" = "Kurulum devam ediyor, lütfen bu sayfayı yenilemeyin" "logs" = "Günlükler" "config" = "Yapılandırma" "backup" = "Yedek" "backupTitle" = "Veritabanı Yedekleme & Geri Yükleme" "exportDatabase" = "Yedekle" "exportDatabaseDesc" = "Mevcut veritabanınızın yedeğini içeren bir .db dosyasını cihazınıza indirmek için tıklayın." "importDatabase" = "Geri Yükle" "importDatabaseDesc" = "Cihazınızdan bir .db dosyası seçip yükleyerek veritabanınızı yedekten geri yüklemek için tıklayın." "importDatabaseSuccess" = "Veritabanı başarıyla içe aktarıldı" "importDatabaseError" = "Veritabanı içe aktarılırken bir hata oluştu" "readDatabaseError" = "Veritabanı okunurken bir hata oluştu" "getDatabaseError" = "Veritabanı alınırken bir hata oluştu" "getConfigError" = "Yapılandırma dosyası alınırken bir hata oluştu" [pages.inbounds] "allTimeTraffic" = "Toplam Trafik" "allTimeTrafficUsage" = "Tüm Zamanların Toplam Kullanımı" "title" = "Gelenler" "totalDownUp" = "Toplam Gönderilen/Alınan" "totalUsage" = "Toplam Kullanım" "inboundCount" = "Toplam Gelen" "operate" = "Menü" "enable" = "Etkin" "remark" = "Açıklama" "protocol" = "Protokol" "port" = "Port" "portMap" = "Port Atama" "traffic" = "Trafik" "details" = "Detaylar" "transportConfig" = "Taşıma" "expireDate" = "Süre" "createdAt" = "Oluşturuldu" "updatedAt" = "Güncellendi" "resetTraffic" = "Trafiği Sıfırla" "addInbound" = "Gelen Ekle" "generalActions" = "Genel Eylemler" "autoRefresh" = "Otomatik yenileme" "autoRefreshInterval" = "Aralık" "modifyInbound" = "Geleni Düzenle" "deleteInbound" = "Geleni Sil" "deleteInboundContent" = "Geleni silmek istediğinizden emin misiniz?" "deleteClient" = "Müşteriyi Sil" "deleteClientContent" = "Müşteriyi silmek istediğinizden emin misiniz?" "resetTrafficContent" = "Trafiği sıfırlamak istediğinizden emin misiniz?" "copyLink" = "URL'yi Kopyala" "address" = "Adres" "network" = "Ağ" "destinationPort" = "Hedef Port" "targetAddress" = "Hedef Adres" "monitorDesc" = "Tüm IP'leri dinlemek için boş bırakın" "meansNoLimit" = "= Sınırsız. (birim: GB)" "totalFlow" = "Toplam Akış" "leaveBlankToNeverExpire" = "Hiçbir zaman sona ermemesi için boş bırakın" "noRecommendKeepDefault" = "Varsayılanı korumanız önerilir" "certificatePath" = "Dosya Yolu" "certificateContent" = "Dosya İçeriği" "publicKey" = "Genel Anahtar" "privatekey" = "Özel Anahtar" "clickOnQRcode" = "Kopyalamak için QR Kodu Tıklayın" "client" = "Müşteri" "export" = "Tüm URL'leri Dışa Aktar" "clone" = "Klonla" "cloneInbound" = "Klonla" "cloneInboundContent" = "Bu gelenin tüm ayarları, Port, Dinleme IP ve Müşteriler hariç, klona uygulanacaktır." "cloneInboundOk" = "Klonla" "resetAllTraffic" = "Tüm Gelen Trafiğini Sıfırla" "resetAllTrafficTitle" = "Tüm Gelen Trafiğini Sıfırla" "resetAllTrafficContent" = "Tüm gelenlerin trafiğini sıfırlamak istediğinizden emin misiniz?" "resetInboundClientTraffics" = "Müşteri Trafiklerini Sıfırla" "resetInboundClientTrafficTitle" = "Müşteri Trafiklerini Sıfırla" "resetInboundClientTrafficContent" = "Bu gelenin müşterilerinin trafiğini sıfırlamak istediğinizden emin misiniz?" "resetAllClientTraffics" = "Tüm Müşteri Trafiklerini Sıfırla" "resetAllClientTrafficTitle" = "Tüm Müşteri Trafiklerini Sıfırla" "resetAllClientTrafficContent" = "Tüm müşterilerin trafiğini sıfırlamak istediğinizden emin misiniz?" "delDepletedClients" = "Bitmiş Müşterileri Sil" "delDepletedClientsTitle" = "Bitmiş Müşterileri Sil" "delDepletedClientsContent" = "Tüm bitmiş müşterileri silmek istediğinizden emin misiniz?" "email" = "E-posta" "emailDesc" = "Lütfen benzersiz bir e-posta adresi sağlayın." "IPLimit" = "IP Limiti" "IPLimitDesc" = "Sayının aşılması durumunda gelen devre dışı bırakılır. (0 = devre dışı)" "IPLimitlog" = "IP Günlüğü" "IPLimitlogDesc" = "IP geçmiş günlüğü. (devre dışı bırakıldıktan sonra gelini etkinleştirmek için günlüğü temizleyin)" "IPLimitlogclear" = "Günlüğü Temizle" "setDefaultCert" = "Panelden Sertifikayı Ayarla" "telegramDesc" = "Lütfen Telegram Sohbet Kimliği sağlayın. (botta '/id' komutunu kullanın) veya (@userinfobot)" "subscriptionDesc" = "Abonelik URL'inizi bulmak için 'Detaylar'a gidin. Ayrıca, aynı adı birden fazla müşteri için kullanabilirsiniz." "info" = "Bilgi" "same" = "Aynı" "inboundData" = "Gelenin Verileri" "exportInbound" = "Geleni Dışa Aktar" "import" = "İçe Aktar" "importInbound" = "Bir Gelen İçe Aktar" "periodicTrafficResetTitle" = "Trafik Sıfırlama" "periodicTrafficResetDesc" = "Belirtilen aralıklarla trafik sayacını otomatik olarak sıfırla" "lastReset" = "Son Sıfırlama" [pages.client] "add" = "Müşteri Ekle" "edit" = "Müşteriyi Düzenle" "submitAdd" = "Müşteri Ekle" "submitEdit" = "Değişiklikleri Kaydet" "clientCount" = "Müşteri Sayısı" "bulk" = "Toplu Ekle" "method" = "Yöntem" "first" = "İlk" "last" = "Son" "prefix" = "Önek" "postfix" = "Sonek" "delayedStart" = "İlk Kullanımdan Sonra Başlat" "expireDays" = "Süre" "days" = "Gün" "renew" = "Otomatik Yenile" "renewDesc" = "Süresi dolduktan sonra otomatik yenileme. (0 = devre dışı)(birim: gün)" [pages.inbounds.periodicTrafficReset] "never" = "Asla" "daily" = "Günlük" "weekly" = "Haftalık" "monthly" = "Aylık" [pages.inbounds.toasts] "obtain" = "Elde Et" "updateSuccess" = "Güncelleme başarılı oldu" "logCleanSuccess" = "Günlük temizlendi" "inboundsUpdateSuccess" = "Gelen bağlantılar başarıyla güncellendi" "inboundUpdateSuccess" = "Gelen bağlantı başarıyla güncellendi" "inboundCreateSuccess" = "Gelen bağlantı başarıyla oluşturuldu" "inboundDeleteSuccess" = "Gelen bağlantı başarıyla silindi" "inboundClientAddSuccess" = "Gelen bağlantı istemci(leri) eklendi" "inboundClientDeleteSuccess" = "Gelen bağlantı istemcisi silindi" "inboundClientUpdateSuccess" = "Gelen bağlantı istemcisi güncellendi" "delDepletedClientsSuccess" = "Tüm tükenmiş istemciler silindi" "resetAllClientTrafficSuccess" = "İstemcinin tüm trafiği sıfırlandı" "resetAllTrafficSuccess" = "Tüm trafik sıfırlandı" "resetInboundClientTrafficSuccess" = "Trafik sıfırlandı" "trafficGetError" = "Trafik bilgisi alınırken hata oluştu" "getNewX25519CertError" = "X25519 sertifikası alınırken hata oluştu." "getNewmldsa65Error" = "mldsa65 sertifikası alınırken hata oluştu." "getNewVlessEncError" = "VlessEnc sertifikası alınırken hata oluştu." [pages.inbounds.stream.general] "request" = "İstek" "response" = "Yanıt" "name" = "Ad" "value" = "Değer" [pages.inbounds.stream.tcp] "version" = "Sürüm" "method" = "Yöntem" "path" = "Yol" "status" = "Durum" "statusDescription" = "Durum Açıklaması" "requestHeader" = "İstek Başlığı" "responseHeader" = "Yanıt Başlığı" [pages.settings] "title" = "Panel Ayarları" "save" = "Kaydet" "infoDesc" = "Burada yapılan her değişikliğin kaydedilmesi gerekir. Değişikliklerin uygulanması için paneli yeniden başlatın." "restartPanel" = "Paneli Yeniden Başlat" "restartPanelDesc" = "Paneli yeniden başlatmak istediğinizden emin misiniz? Yeniden başlattıktan sonra panele erişemezseniz, sunucudaki panel günlük bilgilerini görüntüleyin." "restartPanelSuccess" = "Panel başarıyla yeniden başlatıldı" "actions" = "Eylemler" "resetDefaultConfig" = "Varsayılana Sıfırla" "panelSettings" = "Genel" "securitySettings" = "Kimlik Doğrulama" "TGBotSettings" = "Telegram Bot" "panelListeningIP" = "Dinleme IP" "panelListeningIPDesc" = "Web paneli için IP adresi. (tüm IP'leri dinlemek için boş bırakın)" "panelListeningDomain" = "Dinleme Alan Adı" "panelListeningDomainDesc" = "Web paneli için alan adı. (tüm alan adlarını ve IP'leri dinlemek için boş bırakın)" "panelPort" = "Dinleme Portu" "panelPortDesc" = "Web paneli için port numarası. (kullanılmayan bir port olmalıdır)" "publicKeyPath" = "Genel Anahtar Yolu" "publicKeyPathDesc" = "Web paneli için genel anahtar dosya yolu. ('/' ile başlar)" "privateKeyPath" = "Özel Anahtar Yolu" "privateKeyPathDesc" = "Web paneli için özel anahtar dosya yolu. ('/' ile başlar)" "panelUrlPath" = "URI Yolu" "panelUrlPathDesc" = "Web paneli için URI yolu. ('/' ile başlar ve '/' ile biter)" "pageSize" = "Sayfa Boyutu" "pageSizeDesc" = "Gelenler tablosu için sayfa boyutunu belirleyin. (0 = devre dışı)" "remarkModel" = "Açıklama Modeli & Ayırma Karakteri" "datepicker" = "Takvim Türü" "datepickerPlaceholder" = "Tarih Seçin" "datepickerDescription" = "Planlanmış görevler bu takvime göre çalışacaktır." "sampleRemark" = "Örnek Açıklama" "oldUsername" = "Mevcut Kullanıcı Adı" "currentPassword" = "Mevcut Şifre" "newUsername" = "Yeni Kullanıcı Adı" "newPassword" = "Yeni Şifre" "telegramBotEnable" = "Telegram Botunu Etkinleştir" "telegramBotEnableDesc" = "Telegram botunu etkinleştirir." "telegramToken" = "Telegram Token" "telegramTokenDesc" = "'@BotFather'dan alınan Telegram bot token." "telegramProxy" = "SOCKS Proxy" "telegramProxyDesc" = "Telegram'a bağlanmak için SOCKS5 proxy'sini etkinleştirir. (ayarları kılavuzda belirtilen şekilde ayarlayın)" "telegramAPIServer" = "Telegram API Server" "telegramAPIServerDesc" = "Kullanılacak Telegram API sunucusu. Varsayılan sunucuyu kullanmak için boş bırakın." "telegramChatId" = "Yönetici Sohbet Kimliği" "telegramChatIdDesc" = "Telegram Yönetici Sohbet Kimliği(leri). (virgülle ayrılmış)(buradan alın @userinfobot) veya (botta '/id' komutunu kullanın)" "telegramNotifyTime" = "Bildirim Zamanı" "telegramNotifyTimeDesc" = "Periyodik raporlar için ayarlanan Telegram bot bildirim zamanı. (crontab zaman formatını kullanın)" "tgNotifyBackup" = "Veritabanı Yedeği" "tgNotifyBackupDesc" = "Bir rapor ile birlikte veritabanı yedek dosyasını gönder." "tgNotifyLogin" = "Giriş Bildirimi" "tgNotifyLoginDesc" = "Birisi web panelinize giriş yapmaya çalıştığında kullanıcı adı, IP adresi ve zaman hakkında bildirim alın." "sessionMaxAge" = "Oturum Süresi" "sessionMaxAgeDesc" = "Giriş yaptıktan sonra oturum süresi. (birim: dakika)" "expireTimeDiff" = "Son Kullanma Tarihi Bildirimi" "expireTimeDiffDesc" = "Bu eşik seviyesine ulaşıldığında son kullanma tarihi hakkında bildirim alın. (birim: gün)" "trafficDiff" = "Trafik Sınırı Bildirimi" "trafficDiffDesc" = "Bu eşik seviyesine ulaşıldığında trafik sınırı hakkında bildirim alın. (birim: GB)" "tgNotifyCpu" = "CPU Yükü Bildirimi" "tgNotifyCpuDesc" = "CPU yükü bu eşik seviyesini aşarsa bildirim alın. (birim: %)" "timeZone" = "Saat Dilimi" "timeZoneDesc" = "Planlanmış görevler bu saat dilimine göre çalışacaktır." "subSettings" = "Abonelik" "subEnable" = "Abonelik Hizmetini Etkinleştir" "subEnableDesc" = "Abonelik hizmetini etkinleştirir." "subJsonEnable" = "JSON abonelik uç noktasını bağımsız olarak Etkinleştir/Devre Dışı bırak." "subTitle" = "Abonelik Başlığı" "subTitleDesc" = "VPN istemcisinde gösterilen başlık" "subSupportUrl" = "Destek URL'si" "subSupportUrlDesc" = "VPN istemcisinde gösterilen teknik destek bağlantısı" "subProfileUrl" = "Profil URL'si" "subProfileUrlDesc" = "VPN istemcisinde görüntülenen web sitenize giden bağlantı" "subAnnounce" = "Duyuru" "subAnnounceDesc" = "VPN istemcisinde görüntülenen duyuru metni" "subEnableRouting" = "Yönlendirmeyi etkinleştir" "subEnableRoutingDesc" = "VPN istemcisinde yönlendirmeyi etkinleştirmek için genel ayar. (Yalnızca Happ için)" "subRoutingRules" = "Yönlendirme kuralları" "subRoutingRulesDesc" = "VPN istemcisi için genel yönlendirme kuralları. (Yalnızca Happ için)" "subListen" = "Dinleme IP" "subListenDesc" = "Abonelik hizmeti için IP adresi. (tüm IP'leri dinlemek için boş bırakın)" "subPort" = "Dinleme Portu" "subPortDesc" = "Abonelik hizmeti için port numarası. (kullanılmayan bir port olmalıdır)" "subCertPath" = "Genel Anahtar Yolu" "subCertPathDesc" = "Abonelik hizmeti için genel anahtar dosya yolu. ('/' ile başlar)" "subKeyPath" = "Özel Anahtar Yolu" "subKeyPathDesc" = "Abonelik hizmeti için özel anahtar dosya yolu. ('/' ile başlar)" "subPath" = "URI Yolu" "subPathDesc" = "Abonelik hizmeti için URI yolu. ('/' ile başlar ve '/' ile biter)" "subDomain" = "Dinleme Alan Adı" "subDomainDesc" = "Abonelik hizmeti için alan adı. (tüm alan adlarını ve IP'leri dinlemek için boş bırakın)" "subUpdates" = "Güncelleme Aralıkları" "subUpdatesDesc" = "Müşteri uygulamalarındaki abonelik URL'sinin güncelleme aralıkları. (birim: saat)" "subEncrypt" = "Şifrele" "subEncryptDesc" = "Abonelik hizmetinin döndürülen içeriği Base64 ile şifrelenir." "subShowInfo" = "Kullanım Bilgisini Göster" "subShowInfoDesc" = "Kalan trafik ve tarih müşteri uygulamalarında görüntülenir." "subURI" = "Ters Proxy URI" "subURIDesc" = "Proxy arkasında kullanılacak abonelik URL'sinin URI yolu." "externalTrafficInformEnable" = "Harici Trafik Bilgisi" "externalTrafficInformEnableDesc" = "Her trafik güncellemesinde harici API'yi bilgilendirin." "externalTrafficInformURI" = "Harici Trafik Bilgisi URI'si" "externalTrafficInformURIDesc" = "Trafik güncellemeleri bu URI'ye gönderildi." "fragment" = "Parçalama" "fragmentDesc" = "TLS merhaba paketinin parçalanmasını etkinleştir." "fragmentSett" = "Parçalama Ayarları" "noisesDesc" = "Noises'i Etkinleştir." "noisesSett" = "Noises Ayarları" "mux" = "Mux" "muxDesc" = "Kurulmuş bir veri akışında birden çok bağımsız veri akışını iletir." "muxSett" = "Mux Ayarları" "direct" = "Doğrudan Bağlantı" "directDesc" = "Belirli bir ülkenin alan adları veya IP aralıkları ile doğrudan bağlantı kurar." "notifications" = "Bildirimler" "certs" = "Sertifikalar" "externalTraffic" = "Harici Trafik" "dateAndTime" = "Tarih ve Saat" "proxyAndServer" = "Proxy ve Sunucu" "intervals" = "Aralıklar" "information" = "Bilgi" "language" = "Dil" "telegramBotLanguage" = "Telegram Bot Dili" [pages.xray] "title" = "Xray Yapılandırmaları" "save" = "Kaydet" "restart" = "Xray'i Yeniden Başlat" "restartSuccess" = "Xray başarıyla yeniden başlatıldı" "stopSuccess" = "Xray başarıyla durduruldu" "restartError" = "Xray yeniden başlatılırken bir hata oluştu." "stopError" = "Xray durdurulurken bir hata oluştu." "basicTemplate" = "Temeller" "advancedTemplate" = "Gelişmiş" "generalConfigs" = "Genel" "generalConfigsDesc" = "Bu seçenekler genel ayarlamaları belirler." "logConfigs" = "Günlük" "logConfigsDesc" = "Günlükler sunucunuzun verimliliğini etkileyebilir. Yalnızca ihtiyaç durumunda akıllıca etkinleştirmeniz önerilir" "blockConfigsDesc" = "Bu seçenekler belirli istek protokolleri ve web siteleri temelinde trafiği engeller." "basicRouting" = "Temel Yönlendirme" "blockConnectionsConfigsDesc" = "Bu seçenekler belirli bir istenen ülkeye göre trafiği engelleyecektir." "directConnectionsConfigsDesc" = "Doğrudan bağlantı, belirli bir trafiğin başka bir sunucu üzerinden yönlendirilmediğini sağlar." "blockips" = "IP'leri Engelle" "blockdomains" = "Alan Adlarını Engelle" "directips" = "Doğrudan IP'ler" "directdomains" = "Doğrudan Alan Adları" "ipv4Routing" = "IPv4 Yönlendirme" "ipv4RoutingDesc" = "Bu seçenekler belirli bir varış yerine IPv4 üzerinden trafiği yönlendirir." "warpRouting" = "WARP Yönlendirme" "warpRoutingDesc" = "Bu seçenekler belirli bir varış yerine WARP üzerinden trafiği yönlendirir." "Template" = "Gelişmiş Xray Yapılandırma Şablonu" "TemplateDesc" = "Nihai Xray yapılandırma dosyası bu şablona göre oluşturulacaktır." "FreedomStrategy" = "Freedom Protokol Stratejisi" "FreedomStrategyDesc" = "Freedom Protokolünde ağın çıkış stratejisini ayarlayın." "RoutingStrategy" = "Genel Yönlendirme Stratejisi" "RoutingStrategyDesc" = "Tüm istekleri çözmek için genel trafik yönlendirme stratejisini ayarlayın." "outboundTestUrl" = "Outbound test URL" "outboundTestUrlDesc" = "Outbound bağlantı testinde kullanılan URL" "Torrent" = "BitTorrent Protokolünü Engelle" "Inbounds" = "Gelenler" "InboundsDesc" = "Belirli müşterileri kabul eder." "Outbounds" = "Gidenler" "Balancers" = "Dengeler" "OutboundsDesc" = "Giden trafiğin yolunu ayarlayın." "Routings" = "Yönlendirme Kuralları" "RoutingsDesc" = "Her kuralın önceliği önemlidir!" "completeTemplate" = "Tümü" "logLevel" = "Günlük Seviyesi" "logLevelDesc" = "Hata günlükleri için günlük seviyesi, kaydedilmesi gereken bilgileri belirtir." "accessLog" = "Erişim Günlüğü" "accessLogDesc" = "Erişim günlüğü için dosya yolu. 'none' özel değeri erişim günlüklerini devre dışı bırakır" "errorLog" = "Hata Günlüğü" "errorLogDesc" = "Hata günlüğü için dosya yolu. 'none' özel değeri hata günlüklerini devre dışı bırakır" "dnsLog" = "DNS Günlüğü" "dnsLogDesc" = "DNS sorgu günlüklerini etkinleştirin" "maskAddress" = "Adres Maskesi" "maskAddressDesc" = "IP adresi maskesi, etkinleştirildiğinde, günlükte görünen IP adresini otomatik olarak değiştirecektir." "statistics" = "İstatistikler" "statsInboundUplink" = "Gelen Yükleme İstatistikleri" "statsInboundUplinkDesc" = "Tüm gelen proxy'lerin yükleme trafiği için istatistik toplamayı etkinleştirir." "statsInboundDownlink" = "Gelen İndirme İstatistikleri" "statsInboundDownlinkDesc" = "Tüm gelen proxy'lerin indirme trafiği için istatistik toplamayı etkinleştirir." "statsOutboundUplink" = "Giden Yükleme İstatistikleri" "statsOutboundUplinkDesc" = "Tüm giden proxy'lerin yükleme trafiği için istatistik toplamayı etkinleştirir." "statsOutboundDownlink" = "Giden İndirme İstatistikleri" "statsOutboundDownlinkDesc" = "Tüm giden proxy'lerin indirme trafiği için istatistik toplamayı etkinleştirir." [pages.xray.rules] "first" = "İlk" "last" = "Son" "up" = "Yukarı" "down" = "Aşağı" "source" = "Kaynak" "dest" = "Hedef" "inbound" = "Gelen" "outbound" = "Giden" "balancer" = "Dengeler" "info" = "Bilgi" "add" = "Kural Ekle" "edit" = "Kuralı Düzenle" "useComma" = "Virgülle ayrılmış öğeler" [pages.xray.outbound] "addOutbound" = "Giden Ekle" "addReverse" = "Ters Ekle" "editOutbound" = "Gideni Düzenle" "editReverse" = "Tersi Düzenle" "tag" = "Etiket" "tagDesc" = "Benzersiz Etiket" "address" = "Adres" "reverse" = "Ters" "domain" = "Alan Adı" "type" = "Tür" "bridge" = "Köprü" "portal" = "Portal" "link" = "Bağlantı" "intercon" = "Bağlantı" "settings" = "Ayarlar" "accountInfo" = "Hesap Bilgileri" "outboundStatus" = "Giden Durumu" "sendThrough" = "Üzerinden Gönder" "test" = "Test" "testResult" = "Test Sonucu" "testing" = "Bağlantı test ediliyor..." "testSuccess" = "Test başarılı" "testFailed" = "Test başarısız" "testError" = "Giden test edilemedi" [pages.xray.balancer] "addBalancer" = "Dengeleyici Ekle" "editBalancer" = "Dengeleyiciyi Düzenle" "balancerStrategy" = "Strateji" "balancerSelectors" = "Seçiciler" "tag" = "Etiket" "tagDesc" = "Benzersiz Etiket" "balancerDesc" = "Dengeleyici Etiketi ve Giden Etiketi aynı anda kullanılamaz. Aynı anda kullanıldığında yalnızca giden etiketi çalışır." [pages.xray.wireguard] "secretKey" = "Gizli Anahtar" "publicKey" = "Genel Anahtar" "allowedIPs" = "İzin Verilen IP'ler" "endpoint" = "Uç Nokta" "psk" = "Ön Paylaşılan Anahtar" "domainStrategy" = "Alan Adı Stratejisi" [pages.xray.tun] "nameDesc" = "TUN arabiriminin adı. Varsayılan değer 'xray0'dir" "mtuDesc" = "Maksimum İletim Birimi. Veri paketlerinin maksimum boyutu. Varsayılan değer 1500'dür" "userLevel" = "Kullanıcı Seviyesi" "userLevelDesc" = "Bu giriş yoluyla yapılan tüm bağlantılar bu kullanıcı seviyesini kullanacaktır. Varsayılan değer 0'dır" [pages.xray.dns] "enable" = "DNS'yi Etkinleştir" "enableDesc" = "Dahili DNS sunucusunu etkinleştir" "tag" = "DNS Gelen Etiketi" "tagDesc" = "Bu etiket, yönlendirme kurallarında Gelen etiketi olarak kullanılabilir." "clientIp" = "İstemci IP" "clientIpDesc" = "DNS sorguları sırasında belirtilen IP konumunu sunucuya bildirmek için kullanılır" "disableCache" = "Önbelleği devre dışı bırak" "disableCacheDesc" = "DNS önbelleğini devre dışı bırakır" "disableFallback" = "Yedeklemeyi devre dışı bırak" "disableFallbackDesc" = "Yedek DNS sorgularını devre dışı bırakır" "disableFallbackIfMatch" = "Eşleşirse Yedeklemeyi Devre Dışı Bırak" "disableFallbackIfMatchDesc" = "DNS sunucusunun eşleşen alan adı listesi vurulduğunda yedek DNS sorgularını devre dışı bırakır" "enableParallelQuery" = "Paralel Sorguyu Etkinleştir" "enableParallelQueryDesc" = "Daha hızlı çözümleme için birden fazla sunucuya paralel DNS sorgularını etkinleştir" "strategy" = "Sorgu Stratejisi" "strategyDesc" = "Alan adlarını çözmek için genel strateji" "add" = "Sunucu Ekle" "edit" = "Sunucuyu Düzenle" "domains" = "Alan Adları" "expectIPs" = "Beklenen IP'ler" "unexpectIPs" = "Beklenmeyen IP'ler" "useSystemHosts" = "Sistem Hosts'larını Kullan" "useSystemHostsDesc" = "Yüklü bir sistemden hosts dosyasını kullan" "usePreset" = "Şablon kullan" "dnsPresetTitle" = "DNS Şablonları" "dnsPresetFamily" = "Aile" [pages.xray.fakedns] "add" = "Sahte DNS Ekle" "edit" = "Sahte DNS'i Düzenle" "ipPool" = "IP Havuzu Alt Ağı" "poolSize" = "Havuz Boyutu" [pages.settings.security] "admin" = "Yönetici kimlik bilgileri" "twoFactor" = "İki adımlı doğrulama" "twoFactorEnable" = "2FA'yı Etkinleştir" "twoFactorEnableDesc" = "Daha fazla güvenlik için ek bir doğrulama katmanı ekler." "twoFactorModalSetTitle" = "İki adımlı doğrulamayı etkinleştir" "twoFactorModalDeleteTitle" = "İki adımlı doğrulamayı devre dışı bırak" "twoFactorModalSteps" = "İki adımlı doğrulamayı ayarlamak için şu adımları izleyin:" "twoFactorModalFirstStep" = "1. Bu QR kodunu doğrulama uygulamasında tarayın veya QR kodunun yanındaki token'ı kopyalayıp uygulamaya yapıştırın" "twoFactorModalSecondStep" = "2. Uygulamadaki kodu girin" "twoFactorModalRemoveStep" = "İki adımlı doğrulamayı kaldırmak için uygulamadaki kodu girin." "twoFactorModalChangeCredentialsTitle" = "Kimlik bilgilerini değiştir" "twoFactorModalChangeCredentialsStep" = "Yönetici kimlik bilgilerini değiştirmek için uygulamadaki kodu girin." "twoFactorModalSetSuccess" = "İki faktörlü kimlik doğrulama başarıyla kuruldu" "twoFactorModalDeleteSuccess" = "İki faktörlü kimlik doğrulama başarıyla silindi" "twoFactorModalError" = "Yanlış kod" [pages.settings.toasts] "modifySettings" = "Parametreler değiştirildi." "getSettings" = "Parametreler alınırken bir hata oluştu." "modifyUserError" = "Yönetici kimlik bilgileri değiştirilirken bir hata oluştu." "modifyUser" = "Yönetici kimlik bilgilerini başarıyla değiştirdiniz." "originalUserPassIncorrect" = "Mevcut kullanıcı adı veya şifre geçersiz" "userPassMustBeNotEmpty" = "Yeni kullanıcı adı ve şifre boş olamaz" "getOutboundTrafficError" = "Giden trafik alınırken hata" "resetOutboundTrafficError" = "Giden trafik sıfırlanırken hata" [tgbot] "keyboardClosed" = "❌ Klavye kapatıldı!" "noResult" = "❗ Sonuç yok!" "noQuery" = "❌ Sorgu bulunamadı! Lütfen komutu tekrar kullanın!" "wentWrong" = "❌ Bir şeyler yanlış gitti!" "noIpRecord" = "❗ IP Kaydı Yok!" "noInbounds" = "❗ Gelen bağlantı bulunamadı!" "unlimited" = "♾ Sınırsız (Sıfırla)" "add" = "Ekle" "month" = "Ay" "months" = "Aylar" "day" = "Gün" "days" = "Günler" "hours" = "Saatler" "minutes" = "Dakika" "unknown" = "Bilinmeyen" "inbounds" = "Gelenler" "clients" = "İstemciler" "offline" = "🔴 Çevrimdışı" "online" = "🟢 Çevrimiçi" [tgbot.commands] "unknown" = "❗ Bilinmeyen komut." "pleaseChoose" = "👇 Lütfen seçin:\r\n" "help" = "🤖 Bu bota hoş geldiniz! Web panelinden belirli verileri sunmak ve gerektiğinde değişiklik yapmanıza olanak tanımak için tasarlanmıştır.\r\n\r\n" "start" = "👋 Merhaba {{ .Firstname }}.\r\n" "welcome" = "🤖 {{ .Hostname }} yönetim botuna hoş geldiniz.\r\n" "status" = "✅ Bot çalışıyor!" "usage" = "❗ Lütfen aramak için bir metin sağlayın!" "getID" = "🆔 Kimliğiniz: {{ .ID }}" "helpAdminCommands" = "Xray Core'u yeniden başlatmak için:\r\n/restart\r\n\r\nBir müşteri e-postasını aramak için:\r\n/usage [E-posta]\r\n\r\nGelenleri aramak için (müşteri istatistikleri ile):\r\n/inbound [Açıklama]\r\n\r\nTelegram Sohbet Kimliği:\r\n/id" "helpClientCommands" = "İstatistikleri aramak için şu komutu kullanın:\r\n\r\n/usage [E-posta]\r\n\r\nTelegram Sohbet Kimliği:\r\n/id" "restartUsage" = "\r\n\r\n/restart" "restartSuccess" = "✅ İşlem başarılı!" "restartFailed" = "❗ İşlem hatası.\r\n\r\nHata: {{ .Error }}." "xrayNotRunning" = "❗ Xray Core çalışmıyor." "startDesc" = "Ana menüyü göster" "helpDesc" = "Bot yardımı" "statusDesc" = "Bot durumunu kontrol et" "idDesc" = "Telegram ID'nizi göster" [tgbot.messages] "cpuThreshold" = "🔴 CPU Yükü {{ .Percent }}% eşiği {{ .Threshold }}%'yi aşıyor" "selectUserFailed" = "❌ Kullanıcı seçiminde hata!" "userSaved" = "✅ Telegram Kullanıcısı kaydedildi." "loginSuccess" = "✅ Panele başarıyla giriş yapıldı.\r\n" "loginFailed" = "❗️Panele giriş denemesi başarısız oldu.\r\n" "2faFailed" = "2FA Hatası" "report" = "🕰 Planlanmış Raporlar: {{ .RunTime }}\r\n" "datetime" = "⏰ Tarih&Zaman: {{ .DateTime }}\r\n" "hostname" = "💻 Sunucu: {{ .Hostname }}\r\n" "version" = "🚀 3X-UI Sürümü: {{ .Version }}\r\n" "xrayVersion" = "📡 Xray Sürümü: {{ .XrayVersion }}\r\n" "ipv6" = "🌐 IPv6: {{ .IPv6 }}\r\n" "ipv4" = "🌐 IPv4: {{ .IPv4 }}\r\n" "ip" = "🌐 IP: {{ .IP }}\r\n" "ips" = "🔢 IP'ler:\r\n{{ .IPs }}\r\n" "serverUpTime" = "⏳ Çalışma Süresi: {{ .UpTime }} {{ .Unit }}\r\n" "serverLoad" = "📈 Sistem Yükü: {{ .Load1 }}, {{ .Load2 }}, {{ .Load3 }}\r\n" "serverMemory" = "📋 RAM: {{ .Current }}/{{ .Total }}\r\n" "tcpCount" = "🔹 TCP: {{ .Count }}\r\n" "udpCount" = "🔸 UDP: {{ .Count }}\r\n" "traffic" = "🚦 Trafik: {{ .Total }} (↑{{ .Upload }},↓{{ .Download }})\r\n" "xrayStatus" = "ℹ️ Durum: {{ .State }}\r\n" "username" = "👤 Kullanıcı Adı: {{ .Username }}\r\n" "password" = "👤 Şifre: {{ .Password }}\r\n" "time" = "⏰ Zaman: {{ .Time }}\r\n" "inbound" = "📍 Gelen: {{ .Remark }}\r\n" "port" = "🔌 Port: {{ .Port }}\r\n" "expire" = "📅 Son Kullanma Tarihi: {{ .Time }}\r\n" "expireIn" = "📅 Sona Erecek: {{ .Time }}\r\n" "active" = "💡 Aktif: {{ .Enable }}\r\n" "enabled" = "🚨 Etkin: {{ .Enable }}\r\n" "online" = "🌐 Bağlantı durumu: {{ .Status }}\r\n" "lastOnline" = "🔙 Son çevrimiçi: {{ .Time }}\r\n" "email" = "📧 E-posta: {{ .Email }}\r\n" "upload" = "🔼 Yükleme: ↑{{ .Upload }}\r\n" "download" = "🔽 İndirme: ↓{{ .Download }}\r\n" "total" = "📊 Toplam: ↑↓{{ .UpDown }} / {{ .Total }}\r\n" "TGUser" = "👤 Telegram Kullanıcısı: {{ .TelegramID }}\r\n" "exhaustedMsg" = "🚨 Tükenmiş {{ .Type }}:\r\n" "exhaustedCount" = "🚨 Tükenmiş {{ .Type }} sayısı:\r\n" "onlinesCount" = "🌐 Çevrimiçi Müşteriler: {{ .Count }}\r\n" "disabled" = "🛑 Devre Dışı: {{ .Disabled }}\r\n" "depleteSoon" = "🔜 Yakında Tükenecek: {{ .Deplete }}\r\n\r\n" "backupTime" = "🗄 Yedekleme Zamanı: {{ .Time }}\r\n" "refreshedOn" = "\r\n📋🔄 Yenilendi: {{ .Time }}\r\n\r\n" "yes" = "✅ Evet" "no" = "❌ Hayır" "received_id" = "🔑📥 Kimlik güncellendi." "received_password" = "🔑📥 Şifre güncellendi." "received_email" = "📧📥 E-posta güncellendi." "received_comment" = "💬📥 Yorum güncellendi." "id_prompt" = "🔑 Varsayılan Kimlik: {{ .ClientId }}\n\nKimliğinizi girin." "pass_prompt" = "🔑 Varsayılan Şifre: {{ .ClientPassword }}\n\nŞifrenizi girin." "email_prompt" = "📧 Varsayılan E-posta: {{ .ClientEmail }}\n\nE-postanızı girin." "comment_prompt" = "💬 Varsayılan Yorum: {{ .ClientComment }}\n\nYorumunuzu girin." "inbound_client_data_id" = "🔄 Giriş: {{ .InboundRemark }}\n\n🔑 Kimlik: {{ .ClientId }}\n📧 E-posta: {{ .ClientEmail }}\n📊 Trafik: {{ .ClientTraffic }}\n📅 Bitiş Tarihi: {{ .ClientExp }}\n🌐 IP Sınırı: {{ .IpLimit }}\n💬 Yorum: {{ .ClientComment }}\n\nArtık bu müşteriyi girişe ekleyebilirsin!" "inbound_client_data_pass" = "🔄 Giriş: {{ .InboundRemark }}\n\n🔑 Şifre: {{ .ClientPass }}\n📧 E-posta: {{ .ClientEmail }}\n📊 Trafik: {{ .ClientTraffic }}\n📅 Bitiş Tarihi: {{ .ClientExp }}\n🌐 IP Sınırı: {{ .IpLimit }}\n💬 Yorum: {{ .ClientComment }}\n\nArtık bu müşteriyi girişe ekleyebilirsin!" "cancel" = "❌ İşlem iptal edildi! \n\nİstediğiniz zaman /start ile yeniden başlayabilirsiniz. 🔄" "error_add_client" = "⚠️ Hata:\n\n {{ .error }}" "using_default_value" = "Tamam, varsayılan değeri kullanacağım. 😊" "incorrect_input" = "Girdiğiniz değer geçerli değil.\nKelime öbekleri boşluk olmadan devam etmelidir.\nDoğru örnek: aaaaaa\nYanlış örnek: aaa aaa 🚫" "AreYouSure" = "Emin misin? 🤔" "SuccessResetTraffic" = "📧 E-posta: {{ .ClientEmail }}\n🏁 Sonuç: ✅ Başarılı" "FailedResetTraffic" = "📧 E-posta: {{ .ClientEmail }}\n🏁 Sonuç: ❌ Başarısız \n\n🛠️ Hata: [ {{ .ErrorMessage }} ]" "FinishProcess" = "🔚 Tüm müşteriler için trafik sıfırlama işlemi tamamlandı." [tgbot.buttons] "closeKeyboard" = "❌ Klavyeyi Kapat" "cancel" = "❌ İptal" "cancelReset" = "❌ Sıfırlamayı İptal Et" "cancelIpLimit" = "❌ IP Limitini İptal Et" "confirmResetTraffic" = "✅ Trafiği Sıfırlamayı Onayla?" "confirmClearIps" = "✅ IP'leri Temizlemeyi Onayla?" "confirmRemoveTGUser" = "✅ Telegram Kullanıcısını Kaldırmayı Onayla?" "confirmToggle" = "✅ Kullanıcıyı Etkinleştirme/Devre Dışı Bırakmayı Onayla?" "dbBackup" = "Veritabanı Yedeği Al" "serverUsage" = "Sunucu Kullanımı" "getInbounds" = "Gelenleri Al" "depleteSoon" = "Yakında Tükenecek" "clientUsage" = "Kullanımı Al" "onlines" = "Çevrimiçi Müşteriler" "commands" = "Komutlar" "refresh" = "🔄 Yenile" "clearIPs" = "❌ IP'leri Temizle" "removeTGUser" = "❌ Telegram Kullanıcısını Kaldır" "selectTGUser" = "👤 Telegram Kullanıcısını Seç" "selectOneTGUser" = "👤 Bir Telegram Kullanıcısını Seçin:" "resetTraffic" = "📈 Trafiği Sıfırla" "resetExpire" = "📅 Son Kullanma Tarihini Değiştir" "ipLog" = "🔢 IP Günlüğü" "ipLimit" = "🔢 IP Limiti" "setTGUser" = "👤 Telegram Kullanıcısını Ayarla" "toggle" = "🔘 Etkinleştir / Devre Dışı Bırak" "custom" = "🔢 Özel" "confirmNumber" = "✅ Onayla: {{ .Num }}" "confirmNumberAdd" = "✅ Ekleme onayı: {{ .Num }}" "limitTraffic" = "🚧 Trafik Sınırı" "getBanLogs" = "Yasak Günlüklerini Al" "allClients" = "Tüm Müşteriler" "addClient" = "Müşteri Ekle" "submitDisable" = "Devre Dışı Olarak Gönder ☑️" "submitEnable" = "Etkin Olarak Gönder ✅" "use_default" = "🏷️ Varsayılanı Kullan" "change_id" = "⚙️🔑 Kimlik" "change_password" = "⚙️🔑 Şifre" "change_email" = "⚙️📧 E-posta" "change_comment" = "⚙️💬 Yorum" "ResetAllTraffics" = "Tüm Trafikleri Sıfırla" "SortedTrafficUsageReport" = "Sıralı Trafik Kullanım Raporu" [tgbot.answers] "successfulOperation" = "✅ İşlem başarılı!" "errorOperation" = "❗ İşlemde hata." "getInboundsFailed" = "❌ Gelenler alınamadı." "getClientsFailed" = "❌ Müşteriler alınamadı." "canceled" = "❌ {{ .Email }}: İşlem iptal edildi." "clientRefreshSuccess" = "✅ {{ .Email }}: Müşteri başarıyla yenilendi." "IpRefreshSuccess" = "✅ {{ .Email }}: IP'ler başarıyla yenilendi." "TGIdRefreshSuccess" = "✅ {{ .Email }}: Müşterinin Telegram Kullanıcısı başarıyla yenilendi." "resetTrafficSuccess" = "✅ {{ .Email }}: Trafik başarıyla sıfırlandı." "setTrafficLimitSuccess" = "✅ {{ .Email }}: Trafik limiti başarıyla kaydedildi." "expireResetSuccess" = "✅ {{ .Email }}: Son kullanma günleri başarıyla sıfırlandı." "resetIpSuccess" = "✅ {{ .Email }}: IP limiti {{ .Count }} başarıyla kaydedildi." "clearIpSuccess" = "✅ {{ .Email }}: IP'ler başarıyla temizlendi." "getIpLog" = "✅ {{ .Email }}: IP Günlüğü alındı." "getUserInfo" = "✅ {{ .Email }}: Telegram Kullanıcı Bilgisi alındı." "removedTGUserSuccess" = "✅ {{ .Email }}: Telegram Kullanıcısı başarıyla kaldırıldı." "enableSuccess" = "✅ {{ .Email }}: Başarıyla etkinleştirildi." "disableSuccess" = "✅ {{ .Email }}: Başarıyla devre dışı bırakıldı." "askToAddUserId" = "Yapılandırmanız bulunamadı!\r\nLütfen yöneticinizden yapılandırmalarınıza Telegram ChatID'nizi eklemesini isteyin.\r\n\r\nKullanıcı ChatID'niz: {{ .TgUserID }}" "chooseClient" = "Gelen {{ .Inbound }} için bir Müşteri Seçin" "chooseInbound" = "Bir Gelen Seçin" ================================================ FILE: web/translation/translate.uk_UA.toml ================================================ "username" = "Ім'я користувача" "password" = "Пароль" "login" = "Увійти" "confirm" = "Підтвердити" "cancel" = "Скасувати" "close" = "Закрити" "create" = "Створити" "update" = "Оновити" "copy" = "Копіювати" "copied" = "Скопійовано" "download" = "Завантажити" "remark" = "Примітка" "enable" = "Увімкнути" "protocol" = "Протокол" "search" = "Пошук" "filter" = "Фільтр" "loading" = "Завантаження..." "second" = "Секунда" "minute" = "Хвилина" "hour" = "Година" "day" = "День" "check" = "Перевірка" "indefinite" = "Безстроково" "unlimited" = "Безлімітний" "none" = "Немає" "qrCode" = "QR-Код" "info" = "Більше інформації" "edit" = "Редагувати" "delete" = "Видалити" "reset" = "Скидання" "noData" = "Немає даних." "copySuccess" = "Скопійовано успішно" "sure" = "Звичайно" "encryption" = "Шифрування" "useIPv4ForHost" = "Використовувати IPv4 для хоста" "transmission" = "Протокол передачи" "host" = "Хост" "path" = "Шлях" "camouflage" = "Маскування" "status" = "Статус" "enabled" = "Увімкнено" "disabled" = "Вимкнено" "depleted" = "Вичерпано" "depletingSoon" = "Вичерпується" "offline" = "Офлайн" "online" = "Онлайн" "domainName" = "Доменне ім`я" "monitor" = "Слухати IP" "certificate" = "Цифровий сертифікат" "fail" = "Помилка" "comment" = "Коментар" "success" = "Успішно" "lastOnline" = "Був(ла) онлайн" "getVersion" = "Отримати версію" "install" = "Встановити" "clients" = "Клієнти" "usage" = "Використання" "twoFactorCode" = "Код" "remained" = "Залишилося" "security" = "Беспека" "secAlertTitle" = "Попередження системи безпеки" "secAlertSsl" = "Це з'єднання не є безпечним. Будь ласка, уникайте введення конфіденційної інформації, поки TLS не буде активовано для захисту даних." "secAlertConf" = "Деякі налаштування вразливі до атак. Рекомендується посилити протоколи безпеки, щоб запобігти можливим порушенням." "secAlertSSL" = "Панель не має безпечного з'єднання. Будь ласка, встановіть сертифікат TLS для захисту даних." "secAlertPanelPort" = "Стандартний порт панелі вразливий. Будь ласка, сконфігуруйте випадковий або конкретний порт." "secAlertPanelURI" = "Стандартний URI-шлях панелі небезпечний. Будь ласка, сконфігуруйте складний URI-шлях." "secAlertSubURI" = "Стандартний URI-шлях підписки небезпечний. Будь ласка, сконфігуруйте складний URI-шлях." "secAlertSubJsonURI" = "Стандартний URI-шлях JSON підписки небезпечний. Будь ласка, сконфігуруйте складний URI-шлях." "emptyDnsDesc" = "Немає доданих DNS-серверів." "emptyFakeDnsDesc" = "Немає доданих Fake DNS-серверів." "emptyBalancersDesc" = "Немає доданих балансувальників." "emptyReverseDesc" = "Немає доданих зворотних проксі." "somethingWentWrong" = "Щось пішло не так" [subscription] "title" = "Інформація про підписку" "subId" = "ID підписки" "status" = "Статус" "downloaded" = "Завантажено" "uploaded" = "Відвантажено" "expiry" = "Термін дії" "totalQuota" = "Загальна квота" "individualLinks" = "Окремі посилання" "active" = "Активна" "inactive" = "Неактивна" "unlimited" = "Безліміт" "noExpiry" = "Без строку" [menu] "theme" = "Тема" "dark" = "Темна" "ultraDark" = "Ультра темна" "dashboard" = "Огляд" "inbounds" = "Вхідні" "settings" = "Параметри панелі" "xray" = "Конфігурації Xray" "logout" = "Вийти" "link" = "Керувати" [pages.login] "hello" = "Привіт" "title" = "Привітання!" "loginAgain" = "Ваш сеанс закінчився, увійдіть знову" [pages.login.toasts] "invalidFormData" = "Формат вхідних даних недійсний." "emptyUsername" = "Потрібне ім'я користувача" "emptyPassword" = "Потрібен пароль" "wrongUsernameOrPassword" = "Невірне ім’я користувача, пароль або код двофакторної аутентифікації." "successLogin" = "Ви успішно увійшли до свого облікового запису." [pages.index] "title" = "Огляд" "cpu" = "ЦП" "logicalProcessors" = "Логічні процесори" "frequency" = "Частота" "swap" = "Своп" "storage" = "Сховище" "memory" = "ОЗП" "threads" = "Потоки" "xrayStatus" = "Xray" "stopXray" = "Зупинити" "restartXray" = "Перезапустити" "xraySwitch" = "Версія" "xraySwitchClick" = "Виберіть версію, на яку ви хочете перейти." "xraySwitchClickDesk" = "Вибирайте уважно, оскільки старіші версії можуть бути несумісними з поточними конфігураціями." "xrayStatusUnknown" = "Невідомо" "xrayStatusRunning" = "Запущено" "xrayStatusStop" = "Зупинено" "xrayStatusError" = "Помилка" "xrayErrorPopoverTitle" = "Під час роботи Xray сталася помилка" "operationHours" = "Час роботи" "systemLoad" = "Завантаження системи" "systemLoadDesc" = "Середнє завантаження системи за останні 1, 5 і 15 хвилин" "connectionCount" = "Статистика з'єднання" "ipAddresses" = "IP-адреси" "toggleIpVisibility" = "Перемкнути видимість IP" "overallSpeed" = "Загальна швидкість" "upload" = "Відправка" "download" = "Завантаження" "totalData" = "Загальний обсяг даних" "sent" = "Відправлено" "received" = "Отримано" "documentation" = "Документація" "xraySwitchVersionDialog" = "Ви дійсно хочете змінити версію Xray?" "xraySwitchVersionDialogDesc" = "Це змінить версію Xray на #version#." "xraySwitchVersionPopover" = "Xray успішно оновлено" "geofileUpdateDialog" = "Ви дійсно хочете оновити геофайл?" "geofileUpdateDialogDesc" = "Це оновить файл #filename#." "geofilesUpdateDialogDesc" = "Це оновить усі геофайли." "geofilesUpdateAll" = "Оновити все" "geofileUpdatePopover" = "Геофайл успішно оновлено" "dontRefresh" = "Інсталяція триває, будь ласка, не оновлюйте цю сторінку" "logs" = "Журнали" "config" = "Конфігурація" "backup" = "Резервна копія" "backupTitle" = "Резервне копіювання та відновлення бази даних" "exportDatabase" = "Резервна копія" "exportDatabaseDesc" = "Натисніть, щоб завантажити файл .db, що містить резервну копію вашої поточної бази даних на ваш пристрій." "importDatabase" = "Відновити" "importDatabaseDesc" = "Натисніть, щоб вибрати та завантажити файл .db з вашого пристрою для відновлення бази даних з резервної копії." "importDatabaseSuccess" = "Базу даних успішно імпортовано" "importDatabaseError" = "Виникла помилка під час імпорту бази даних" "readDatabaseError" = "Виникла помилка під час читання бази даних" "getDatabaseError" = "Виникла помилка під час отримання бази даних" "getConfigError" = "Виникла помилка під час отримання файлу конфігурації" [pages.inbounds] "allTimeTraffic" = "Загальний трафік" "allTimeTrafficUsage" = "Загальне використання за весь час" "title" = "Вхідні" "totalDownUp" = "Всього надісланих/отриманих" "totalUsage" = "Всього використанно" "inboundCount" = "Загальна кількість вхідних" "operate" = "Меню" "enable" = "Увімкнено" "remark" = "Примітка" "protocol" = "Протокол" "port" = "Порт" "portMap" = "Порт-перехід" "traffic" = "Трафік" "details" = "Деталі" "transportConfig" = "Транспорт" "expireDate" = "Тривалість" "createdAt" = "Створено" "updatedAt" = "Оновлено" "resetTraffic" = "Скинути трафік" "addInbound" = "Додати вхідний" "generalActions" = "Загальні дії" "autoRefresh" = "Автооновлення" "autoRefreshInterval" = "Інтервал" "modifyInbound" = "Змінити вхідний" "deleteInbound" = "Видалити вхідні" "deleteInboundContent" = "Ви впевнені, що хочете видалити вхідні?" "deleteClient" = "Видалити клієнта" "deleteClientContent" = "Ви впевнені, що хочете видалити клієнт?" "resetTrafficContent" = "Ви впевнені, що хочете скинути трафік?" "copyLink" = "Копіювати URL" "address" = "Адреса" "network" = "Мережа" "destinationPort" = "Порт призначення" "targetAddress" = "Цільова адреса" "monitorDesc" = "Залиште порожнім, щоб слухати всі IP-адреси" "meansNoLimit" = "= Необмежено. (одиниця: ГБ)" "totalFlow" = "Загальна витрата" "leaveBlankToNeverExpire" = "Залиште порожнім, щоб ніколи не закінчувався" "noRecommendKeepDefault" = "Рекомендується зберегти значення за замовчуванням" "certificatePath" = "Шлях до файлу" "certificateContent" = "Вміст файлу" "publicKey" = "Публічний ключ" "privatekey" = "Закритий ключ" "clickOnQRcode" = "Натисніть QR-код, щоб скопіювати" "client" = "Клієнт" "export" = "Експортувати всі URL-адреси" "clone" = "Клон" "cloneInbound" = "Клонувати" "cloneInboundContent" = "Усі налаштування цього вхідного потоку, крім порту, IP-адреси прослуховування та клієнтів, будуть застосовані до клону." "cloneInboundOk" = "Клонувати" "resetAllTraffic" = "Скинути весь вхідний трафік" "resetAllTrafficTitle" = "Скинути весь вхідний трафік" "resetAllTrafficContent" = "Ви впевнені, що бажаєте скинути трафік усіх вхідних?" "resetInboundClientTraffics" = "Скинути трафік клієнтів" "resetInboundClientTrafficTitle" = "Скинути трафік клієнтів" "resetInboundClientTrafficContent" = "Ви впевнені, що бажаєте скинути трафік клієнтів цього вхідного потоку?" "resetAllClientTraffics" = "Скинути весь трафік клієнтів" "resetAllClientTrafficTitle" = "Скинути весь трафік клієнтів" "resetAllClientTrafficContent" = "Ви впевнені, що бажаєте скинути трафік усіх клієнтів?" "delDepletedClients" = "Видалити вичерпані клієнти" "delDepletedClientsTitle" = "Видалити вичерпані клієнти" "delDepletedClientsContent" = "Ви впевнені, що хочете видалити всі вичерпані клієнти?" "email" = "Електронна пошта" "emailDesc" = "Будь ласка, надайте унікальну адресу електронної пошти." "IPLimit" = "Обмеження IP" "IPLimitDesc" = "Вимикає вхідний, якщо кількість перевищує встановлене значення. (0 = вимкнено)" "IPLimitlog" = "Журнал IP" "IPLimitlogDesc" = "Журнал історії IP-адрес. (щоб увімкнути вхідну після вимкнення, очистіть журнал)" "IPLimitlogclear" = "Очистити журнал" "setDefaultCert" = "Установити сертифікат з панелі" "telegramDesc" = "Будь ласка, вкажіть ID чату Telegram. (використовуйте команду '/id' у боті) або (@userinfobot)" "subscriptionDesc" = "Щоб знайти URL-адресу вашої підписки, перейдіть до «Деталі». Крім того, ви можете використовувати одне ім'я для кількох клієнтів." "info" = "Інформація" "same" = "Те саме" "inboundData" = "Вхідні дані" "exportInbound" = "Експортувати вхідні" "import" = "Імпорт" "importInbound" = "Імпортувати вхідний" "periodicTrafficResetTitle" = "Скидання трафіку" "periodicTrafficResetDesc" = "Автоматично скидати лічильник трафіку через певні проміжки часу" "lastReset" = "Останнє скидання" [pages.client] "add" = "Додати клієнта" "edit" = "Редагувати клієнта" "submitAdd" = "Додати клієнта" "submitEdit" = "Зберегти зміни" "clientCount" = "Кількість клієнтів" "bulk" = "Додати групу" "method" = "Метод" "first" = "Перший" "last" = "Останній" "prefix" = "Префікс" "postfix" = "Постфікс" "delayedStart" = "Початок використання" "expireDays" = "Тривалість" "days" = "Дні(в)" "renew" = "Автоматичне оновлення" "renewDesc" = "Автоматичне поновлення після закінчення терміну дії. (0 = вимкнено)(одиниця: день)" [pages.inbounds.periodicTrafficReset] "never" = "Ніколи" "daily" = "Щодня" "weekly" = "Щотижня" "monthly" = "Щомісяця" [pages.inbounds.toasts] "obtain" = "Отримати" "updateSuccess" = "Оновлення пройшло успішно" "logCleanSuccess" = "Журнал очищено" "inboundsUpdateSuccess" = "Вхідні підключення успішно оновлено" "inboundUpdateSuccess" = "Вхідне підключення успішно оновлено" "inboundCreateSuccess" = "Вхідне підключення успішно створено" "inboundDeleteSuccess" = "Вхідне підключення успішно видалено" "inboundClientAddSuccess" = "Клієнт(и) вхідного підключення додано" "inboundClientDeleteSuccess" = "Клієнта вхідного підключення видалено" "inboundClientUpdateSuccess" = "Клієнта вхідного підключення оновлено" "delDepletedClientsSuccess" = "Усі вичерпані клієнти видалені" "resetAllClientTrafficSuccess" = "Весь трафік клієнта скинуто" "resetAllTrafficSuccess" = "Весь трафік скинуто" "resetInboundClientTrafficSuccess" = "Трафік скинуто" "trafficGetError" = "Помилка отримання даних про трафік" "getNewX25519CertError" = "Помилка при отриманні сертифіката X25519." "getNewmldsa65Error" = "Помилка при отриманні сертифіката mldsa65." "getNewVlessEncError" = "Помилка при отриманні сертифіката VlessEnc." [pages.inbounds.stream.general] "request" = "Запит" "response" = "Відповідь" "name" = "Ім'я" "value" = "Значення" [pages.inbounds.stream.tcp] "version" = "Версія" "method" = "Метод" "path" = "Шлях" "status" = "Статус" "statusDescription" = "Опис стану" "requestHeader" = "Заголовок запиту" "responseHeader" = "Заголовок відповіді" [pages.settings] "title" = "Параметри панелі" "save" = "Зберегти" "infoDesc" = "Кожна внесена тут зміна повинна бути збережена. Перезапустіть панель, щоб застосувати зміни." "restartPanel" = "Перезапустити панель" "restartPanelDesc" = "Ви впевнені, що бажаєте перезапустити панель? Якщо ви не можете отримати доступ до панелі після перезапуску, будь ласка, перегляньте інформацію журналу панелі на сервері." "restartPanelSuccess" = "Панель успішно перезапущено" "actions" = "Дії" "resetDefaultConfig" = "Відновити значення за замовчуванням" "panelSettings" = "Загальні" "securitySettings" = "Автентифікація" "TGBotSettings" = "Telegram Бот" "panelListeningIP" = "Слухати IP" "panelListeningIPDesc" = "IP-адреса для веб-панелі. (залиште порожнім, щоб слухати всі IP-адреси)" "panelListeningDomain" = "Домен прослуховування" "panelListeningDomainDesc" = "Доменне ім'я для веб-панелі. (залиште порожнім, щоб слухати всі домени та IP-адреси)" "panelPort" = "Порт прослуховування" "panelPortDesc" = "Номер порту для веб-панелі. (має бути невикористаний порт)" "publicKeyPath" = "Шлях відкритого ключа" "publicKeyPathDesc" = "Шлях до файлу відкритого ключа для веб-панелі. (починається з ‘/‘)" "privateKeyPath" = "Шлях приватного ключа" "privateKeyPathDesc" = "Шлях до файлу приватного ключа для веб-панелі. (починається з ‘/‘)" "panelUrlPath" = "Шлях URL" "panelUrlPathDesc" = "Шлях URL для веб-панелі. (починається з ‘/‘ і закінчується ‘/‘)" "pageSize" = "Розмір сторінки" "pageSizeDesc" = "Визначити розмір сторінки для вхідної таблиці. (0 = вимкнено)" "remarkModel" = "Модель зауваження та роздільний символ" "datepicker" = "Тип календаря" "datepickerPlaceholder" = "Виберіть дату" "datepickerDescription" = "Заплановані завдання виконуватимуться на основі цього календаря." "sampleRemark" = "Зразок зауваження" "oldUsername" = "Поточне ім'я користувача" "currentPassword" = "Поточний пароль" "newUsername" = "Нове ім'я користувача" "newPassword" = "Новий пароль" "telegramBotEnable" = "Увімкнути Telegram Bot" "telegramBotEnableDesc" = "Вмикає бота Telegram." "telegramToken" = "Telegram Токен" "telegramTokenDesc" = "Токен бота Telegram, отриманий від '@BotFather'." "telegramProxy" = "SOCKS Проксі" "telegramProxyDesc" = "Вмикає проксі-сервер SOCKS5 для підключення до Telegram. (відкоригуйте параметри відповідно до посібника)" "telegramAPIServer" = "Сервер Telegram API" "telegramAPIServerDesc" = "Сервер Telegram API для використання. Залиште поле порожнім, щоб використовувати сервер за умовчанням." "telegramChatId" = "Ідентифікатор чату адміністратора" "telegramChatIdDesc" = "Ідентифікатори чату адміністратора Telegram. (розділені комами) (отримайте тут @userinfobot) або (використовуйте команду '/id' у боті)" "telegramNotifyTime" = "Час сповіщення" "telegramNotifyTimeDesc" = "Час повідомлення бота Telegram, встановлений для періодичних звітів. (використовуйте формат часу crontab)" "tgNotifyBackup" = "Резервне копіювання бази даних" "tgNotifyBackupDesc" = "Надіслати файл резервної копії бази даних зі звітом." "tgNotifyLogin" = "Сповіщення про вхід" "tgNotifyLoginDesc" = "Отримувати сповіщення про ім'я користувача, IP-адресу та час щоразу, коли хтось намагається увійти у вашу веб-панель." "sessionMaxAge" = "Тривалість сеансу" "sessionMaxAgeDesc" = "Тривалість, протягом якої ви можете залишатися в системі. (одиниця: хвилина)" "expireTimeDiff" = "Повідомлення про дату закінчення" "expireTimeDiffDesc" = "Отримувати сповіщення про термін дії при досягненні цього порогу. (одиниця: день)" "trafficDiff" = "Повідомлення про обмеження трафіку" "trafficDiffDesc" = "Отримувати сповіщення про обмеження трафіку при досягненні цього порогу. (одиниця: ГБ)" "tgNotifyCpu" = "Сповіщення про завантаження ЦП" "tgNotifyCpuDesc" = "Отримувати сповіщення, якщо навантаження ЦП перевищує це порогове значення. (одиниця: %)" "timeZone" = "Часовий пояс" "timeZoneDesc" = "Заплановані завдання виконуватимуться на основі цього часового поясу." "subSettings" = "Підписка" "subEnable" = "Увімкнути службу підписки" "subEnableDesc" = "Вмикає службу підписки." "subJsonEnable" = "Увімкнути/вимкнути JSON-кінець підписки незалежно." "subTitle" = "Назва Підписки" "subTitleDesc" = "Назва, яка відображається у VPN-клієнті" "subSupportUrl" = "URL підтримки" "subSupportUrlDesc" = "Посилання на технічну підтримку, що відображається у VPN-клієнті" "subProfileUrl" = "URL профілю" "subProfileUrlDesc" = "Посилання на ваш вебсайт, що відображається у VPN-клієнті" "subAnnounce" = "Оголошення" "subAnnounceDesc" = "Текст оголошення, що відображається у VPN-клієнті" "subEnableRouting" = "Увімкнути маршрутизацію" "subEnableRoutingDesc" = "Глобальне налаштування для увімкнення маршрутизації у VPN-клієнті. (Тільки для Happ)" "subRoutingRules" = "Правила маршрутизації" "subRoutingRulesDesc" = "Глобальні правила маршрутизації для VPN-клієнта. (Тільки для Happ)" "subListen" = "Слухати IP" "subListenDesc" = "IP-адреса для служби підписки. (залиште порожнім, щоб слухати всі IP-адреси)" "subPort" = "Слухати порт" "subPortDesc" = "Номер порту для служби підписки. (має бути невикористаний порт)" "subCertPath" = "Шлях відкритого ключа" "subCertPathDesc" = "Шлях до файлу відкритого ключа для служби підписки. (починається з ‘/‘)" "subKeyPath" = "Шлях приватного ключа" "subKeyPathDesc" = "Шлях до файлу приватного ключа для служби підписки. (починається з ‘/‘)" "subPath" = "Шлях URI" "subPathDesc" = "Шлях URI для служби підписки. (починається з ‘/‘ і закінчується ‘/‘)" "subDomain" = "Домен прослуховування" "subDomainDesc" = "Ім'я домену для служби підписки. (залиште порожнім, щоб слухати всі домени та IP-адреси)" "subUpdates" = "Інтервали оновлення" "subUpdatesDesc" = "Інтервали оновлення URL-адреси підписки в клієнтських програмах. (одиниця: година)" "subEncrypt" = "Закодувати" "subEncryptDesc" = "Повернений вміст послуги підписки матиме кодування Base64." "subShowInfo" = "Показати інформацію про використання" "subShowInfoDesc" = "Залишок трафіку та дата відображатимуться в клієнтських програмах." "subURI" = "URI зворотного проксі" "subURIDesc" = "URI до URL-адреси підписки для використання за проксі." "externalTrafficInformEnable" = "Інформація про зовнішній трафік" "externalTrafficInformEnableDesc" = "Інформувати зовнішній API про кожне оновлення трафіку." "externalTrafficInformURI" = "Інформаційний URI зовнішнього трафіку" "externalTrafficInformURIDesc" = "Оновлення трафіку надсилаються на цей URI." "fragment" = "Фрагментація" "fragmentDesc" = "Увімкнути фрагментацію для пакету привітання TLS" "fragmentSett" = "Параметри фрагментації" "noisesDesc" = "Увімкнути Noises." "noisesSett" = "Налаштування Noises" "mux" = "Mux" "muxDesc" = "Передавати кілька незалежних потоків даних у межах встановленого потоку даних." "muxSett" = "Налаштування Mux" "direct" = "Пряме підключення" "directDesc" = "Безпосередньо встановлює з’єднання з доменами або діапазонами IP певної країни." "notifications" = "Сповіщення" "certs" = "Сертифікати" "externalTraffic" = "Зовнішній трафік" "dateAndTime" = "Дата та час" "proxyAndServer" = "Проксі та сервер" "intervals" = "Інтервали" "information" = "Інформація" "language" = "Мова" "telegramBotLanguage" = "Мова Telegram-бота" [pages.xray] "title" = "Xray конфігурації" "save" = "Зберегти" "restart" = "Перезапустити Xray" "restartSuccess" = "Xray успішно перезапущено" "stopSuccess" = "Xray успішно зупинено" "restartError" = "Виникла помилка під час перезапуску Xray." "stopError" = "Виникла помилка під час зупинки Xray." "basicTemplate" = "Базовий шаблон" "advancedTemplate" = "Додатково" "generalConfigs" = "Загальні конфігурації" "generalConfigsDesc" = "Ці параметри визначатимуть загальні налаштування." "logConfigs" = "Журнал" "logConfigsDesc" = "Журнали можуть вплинути на ефективність вашого сервера. Рекомендується вмикати його з розумом лише у випадку ваших потреб" "blockConfigsDesc" = "Ці параметри блокуватимуть трафік на основі конкретних запитуваних протоколів і веб-сайтів." "basicRouting" = "Основна Маршрутизація" "blockConnectionsConfigsDesc" = "Ці параметри блокуватимуть трафік на основі запитаних країн." "directConnectionsConfigsDesc" = "Пряме з'єднання гарантує, що певний трафік не буде маршрутизовано через інший сервер." "blockips" = "Блокувати IP" "blockdomains" = "Блокувати домени" "directips" = "Прямі IP" "directdomains" = "Прямі домени" "ipv4Routing" = "Маршрутизація IPv4" "ipv4RoutingDesc" = "Ці параметри спрямовуватимуть трафік на основі певного призначення через IPv4." "warpRouting" = "WARP Маршрутизація" "warpRoutingDesc" = "Ці параметри маршрутизуватимуть трафік на основі певного пункту призначення через WARP." "Template" = "Шаблон розширеної конфігурації Xray" "TemplateDesc" = "Остаточний конфігураційний файл Xray буде створено на основі цього шаблону." "FreedomStrategy" = "Стратегія протоколу свободи" "FreedomStrategyDesc" = "Установити стратегію виведення для мережі в протоколі свободи." "RoutingStrategy" = "Загальна стратегія маршрутизації" "RoutingStrategyDesc" = "Установити загальну стратегію маршрутизації трафіку для вирішення всіх запитів." "outboundTestUrl" = "URL тесту outbound" "outboundTestUrlDesc" = "URL для перевірки з'єднання outbound" "Torrent" = "Блокувати протокол BitTorrent" "Inbounds" = "Вхідні" "InboundsDesc" = "Прийняття певних клієнтів." "Outbounds" = "Вихід" "Balancers" = "Балансери" "OutboundsDesc" = "Встановити шлях вихідного трафіку." "Routings" = "Правила маршрутизації" "RoutingsDesc" = "Пріоритет кожного правила важливий!" "completeTemplate" = "Усі" "logLevel" = "Рівень журналу" "logLevelDesc" = "Рівень журналу для журналів помилок із зазначенням інформації, яку потрібно записати." "accessLog" = "Журнал доступу" "accessLogDesc" = "Шлях до файлу журналу доступу. Спеціальне значення 'none' вимикає журнали доступу" "errorLog" = "Журнал помилок" "errorLogDesc" = "Шлях до файлу журналу помилок. Спеціальне значення 'none' вимикає журнали помилок" "dnsLog" = "Журнал DNS" "dnsLogDesc" = "Чи включити журнали запитів DNS" "maskAddress" = "Маскувати Адресу" "maskAddressDesc" = "Маска IP-адреси, при активації автоматично замінює IP-адресу, яка з'являється у журналі." "statistics" = "Статистика" "statsInboundUplink" = "Статистика вхідного аплінку" "statsInboundUplinkDesc" = "Увімкнення збору статистики для вхідного трафіку всіх вхідних проксі." "statsInboundDownlink" = "Статистика вхідного даунлінку" "statsInboundDownlinkDesc" = "Увімкнення збору статистики для вихідного трафіку всіх вхідних проксі." "statsOutboundUplink" = "Статистика вихідного аплінку" "statsOutboundUplinkDesc" = "Увімкнення збору статистики для вхідного трафіку всіх вихідних проксі." "statsOutboundDownlink" = "Статистика вихідного даунлінку" "statsOutboundDownlinkDesc" = "Увімкнення збору статистики для вихідного трафіку всіх вихідних проксі." [pages.xray.rules] "first" = "Перший" "last" = "Останній" "up" = "Вгору" "down" = "Вниз" "source" = "Джерело" "dest" = "Пункт призначення" "inbound" = "Вхідний" "outbound" = "Вихідний" "balancer" = "Балансувальник" "info" = "Інформація" "add" = "Додати правило" "edit" = "Редагувати правило" "useComma" = "Елементи, розділені комами" [pages.xray.outbound] "addOutbound" = "Додати вихідний" "addReverse" = "Додати реверс" "editOutbound" = "Редагувати вихідні" "editReverse" = "Редагувати реверс" "tag" = "Тег" "tagDesc" = "Унікальний тег" "address" = "Адреса" "reverse" = "Зворотний" "domain" = "Домен" "type" = "Тип" "bridge" = "Міст" "portal" = "Портал" "link" = "Посилання" "intercon" = "Взаємозв'язок" "settings" = "Налаштування" "accountInfo" = "Інформація про обліковий запис" "outboundStatus" = "Статус виходу" "sendThrough" = "Надіслати через" "test" = "Тест" "testResult" = "Результат тесту" "testing" = "Тестування з'єднання..." "testSuccess" = "Тест успішний" "testFailed" = "Тест не пройдено" "testError" = "Не вдалося протестувати вихідне з'єднання" [pages.xray.balancer] "addBalancer" = "Додати балансир" "editBalancer" = "Редагувати балансир" "balancerStrategy" = "Стратегія" "balancerSelectors" = "Селектори" "tag" = "Тег" "tagDesc" = "Унікальний тег" "balancerDesc" = "Неможливо використовувати balancerTag і outboundTag одночасно. Якщо використовувати одночасно, працюватиме лише outboundTag." [pages.xray.wireguard] "secretKey" = "Приватний ключ" "publicKey" = "Публічний ключ" "allowedIPs" = "Дозволені IP-адреси" "endpoint" = "Кінцева точка" "psk" = "Спільний ключ" "domainStrategy" = "Стратегія домену" [pages.xray.tun] "nameDesc" = "Назва інтерфейсу TUN. Значення за замовчуванням - 'xray0'" "mtuDesc" = "Максимальна одиниця передачі. Максимальний розмір пакетів даних. Значення за замовчуванням - 1500" "userLevel" = "Рівень користувача" "userLevelDesc" = "Всі з'єднання, встановлені через цей вхід, використовуватимуть цей рівень користувача. Значення за замовчуванням - 0" [pages.xray.dns] "enable" = "Увімкнути DNS" "enableDesc" = "Увімкнути вбудований DNS-сервер" "tag" = "Мітка вхідного DNS" "tagDesc" = "Ця мітка буде доступна як вхідна мітка в правилах маршрутизації." "clientIp" = "IP клієнта" "clientIpDesc" = "Використовується для повідомлення серверу про вказане місцезнаходження IP під час DNS-запитів" "disableCache" = "Вимкнути кеш" "disableCacheDesc" = "Вимкнути кешування DNS" "disableFallback" = "Вимкнути резервний DNS" "disableFallbackDesc" = "Вимкнути резервні DNS-запити" "disableFallbackIfMatch" = "Вимкнути резервний DNS при збігу" "disableFallbackIfMatchDesc" = "Вимкнути резервні DNS-запити при збігу списку доменів DNS-сервера" "enableParallelQuery" = "Увімкнути паралельні запити" "enableParallelQueryDesc" = "Увімкнути паралельні DNS-запити до кількох серверів для швидшого вирішення" "strategy" = "Стратегія запиту" "strategyDesc" = "Загальна стратегія вирішення доменних імен" "add" = "Додати сервер" "edit" = "Редагувати сервер" "domains" = "Домени" "expectIPs" = "Очікувані IP" "unexpectIPs" = "Неочікувані IP" "useSystemHosts" = "Використовувати системні Hosts" "useSystemHostsDesc" = "Використовувати файл hosts з встановленої системи" "usePreset" = "Використати шаблон" "dnsPresetTitle" = "Шаблони DNS" "dnsPresetFamily" = "Сімейний" [pages.xray.fakedns] "add" = "Додати підроблений DNS" "edit" = "Редагувати підроблений DNS" "ipPool" = "Підмережа IP-пулу" "poolSize" = "Розмір пулу" [pages.settings.security] "admin" = "Облікові дані адміністратора" "twoFactor" = "Двофакторна аутентифікація" "twoFactorEnable" = "Увімкнути 2FA" "twoFactorEnableDesc" = "Додає додатковий рівень аутентифікації для підвищення безпеки." "twoFactorModalSetTitle" = "Увімкнути двофакторну аутентифікацію" "twoFactorModalDeleteTitle" = "Вимкнути двофакторну аутентифікацію" "twoFactorModalSteps" = "Щоб налаштувати двофакторну аутентифікацію, виконайте кілька кроків:" "twoFactorModalFirstStep" = "1. Відскануйте цей QR-код у програмі для аутентифікації або скопіюйте токен біля QR-коду та вставте його в програму" "twoFactorModalSecondStep" = "2. Введіть код з програми" "twoFactorModalRemoveStep" = "Введіть код з програми, щоб вимкнути двофакторну аутентифікацію." "twoFactorModalChangeCredentialsTitle" = "Змінити облікові дані" "twoFactorModalChangeCredentialsStep" = "Введіть код з додатку, щоб змінити облікові дані адміністратора." "twoFactorModalSetSuccess" = "Двофакторна аутентифікація була успішно встановлена" "twoFactorModalDeleteSuccess" = "Двофакторна аутентифікація була успішно видалена" "twoFactorModalError" = "Невірний код" [pages.settings.toasts] "modifySettings" = "Параметри було змінено." "getSettings" = "Виникла помилка під час отримання параметрів." "modifyUserError" = "Виникла помилка під час зміни облікових даних адміністратора." "modifyUser" = "Ви успішно змінили облікові дані адміністратора." "originalUserPassIncorrect" = "Поточне ім'я користувача або пароль недійсні" "userPassMustBeNotEmpty" = "Нове ім'я користувача та пароль порожні" "getOutboundTrafficError" = "Помилка отримання вихідного трафіку" "resetOutboundTrafficError" = "Помилка скидання вихідного трафіку" [tgbot] "keyboardClosed" = "❌ Клавіатуру закрито!" "noResult" = "❗ Немає результату!" "noQuery" = "❌ Запит не знайдено! Будь ласка, використовуйте команду ще раз!" "wentWrong" = "❌ Щось пішло не так!" "noIpRecord" = "❗ Немає запису IP!" "noInbounds" = "❗ Вхідні не знайдені!" "unlimited" = "♾ Необмежено (Скинути)" "add" = "Додати" "month" = "Місяць" "months" = "Місяці" "day" = "День" "days" = "Дні" "hours" = "Години" "minutes" = "Хвилини" "unknown" = "Невідомо" "inbounds" = "Вхідні" "clients" = "Клієнти" "offline" = "🔴 Офлайн" "online" = "🟢 Онлайн" [tgbot.commands] "unknown" = "❗ Невідома команда." "pleaseChoose" = "👇 Будь ласка, виберіть:\r\n" "help" = "🤖 Ласкаво просимо до цього бота! Він розроблений, щоб надавати певні дані з веб-панелі та дозволяє вносити зміни за потреби.\r\n\r\n" "start" = "👋 Привіт {{ .Firstname }}.\r\n" "welcome" = "🤖 Ласкаво просимо до {{ .Hostname }} бота керування.\r\n" "status" = "✅ Бот в порядку!" "usage" = "❗ Введіть текст для пошуку!" "getID" = "🆔 Ваш ідентифікатор: {{ .ID }}" "helpAdminCommands" = "Для перезапуску Xray Core:\r\n/restart\r\n\r\nДля пошуку електронної пошти клієнта:\r\n/usage [Електронна пошта]\r\n\r\nДля пошуку вхідних (зі статистикою клієнта):\r\n/inbound [Примітка]\r\n\r\nID чату Telegram:\r\n/id" "helpClientCommands" = "Для пошуку статистики використовуйте наступну команду:\r\n/usage [Електронна пошта]\r\n\r\nID чату Telegram:\r\n/id" "restartUsage" = "\r\n\r\n/restart" "restartSuccess" = "✅ Операція успішна!" "restartFailed" = "❗ Помилка в операції.\r\n\r\nПомилка: {{ .Error }}." "xrayNotRunning" = "❗ Xray Core не запущений." "startDesc" = "Показати головне меню" "helpDesc" = "Довідка по боту" "statusDesc" = "Перевірити статус бота" "idDesc" = "Показати ваш Telegram ID" [tgbot.messages] "cpuThreshold" = "🔴 Навантаження ЦП {{ .Percent }}% перевищує порогове значення {{ .Threshold }}%" "selectUserFailed" = "❌ Помилка під час вибору користувача!" "userSaved" = "✅ Користувача Telegram збережено." "loginSuccess" = "✅ Успішно ввійшли в панель\r\n" "loginFailed" = "❗️ Помилка входу в панель.\r\n" "2faFailed" = "Помилка 2FA" "report" = "🕰 Заплановані звіти: {{ .RunTime }}\r\n" "datetime" = "⏰ Дата й час: {{ .DateTime }}\r\n" "hostname" = "💻 Хост: {{ .Hostname }}\r\n" "version" = "🚀 3X-UI Версія: {{ .Version }}\r\n" "xrayVersion" = "📡 Xray Версія: {{ .XrayVersion }}\r\n" "ipv6" = "🌐 IPv6: {{ .IPv6 }}\r\n" "ipv4" = "🌐 IPv4: {{ .IPv4 }}\r\n" "ip" = "🌐 IP: {{ .IP }}\r\n" "ips" = "🔢 IP-адреси:\r\n{{ .IPs }}\r\n" "serverUpTime" = "⏳ Час роботи: {{ .UpTime }} {{ .Unit }}\r\n" "serverLoad" = "📈 Завантаження системи: {{ .Load1 }}, {{ .Load2 }}, {{ .Load3 }}\r\n" "serverMemory" = "📋 RAM: {{ .Current }}/{{ .Total }}\r\n" "tcpCount" = "🔹 TCP: {{ .Count }}\r\n" "udpCount" = "🔸 UDP: {{ .Count }}\r\n" "traffic" = "🚦 Трафік: {{ .Total }} (↑{{ .Upload }},↓{{ .Download }})\r\n" "xrayStatus" = "ℹ️ Статус: {{ .State }}\r\n" "username" = "👤 Ім'я користувача: {{ .Username }}\r\n" "password" = "👤 Пароль: {{ .Password }}\r\n" "time" = "⏰ Час: {{ .Time }}\r\n" "inbound" = "📍 Inbound: {{ .Remark }}\r\n" "port" = "🔌 Порт: {{ .Port }}\r\n" "expire" = "📅 Дата закінчення: {{ .Time }}\r\n" "expireIn" = "📅 Термін дії: {{ .Time }}\r\n" "active" = "💡 Активний: {{ .Enable }}\r\n" "enabled" = "🚨 Увімкнено: {{ .Enable }}\r\n" "online" = "🌐 Стан підключення: {{ .Status }}\r\n" "lastOnline" = "🔙 Був(ла) онлайн: {{ .Time }}\r\n" "email" = "📧 Електронна пошта: {{ .Email }}\r\n" "upload" = "🔼 Upload: ↑{{ .Upload }}\r\n" "download" = "🔽 Download: ↓{{ .Download }}\r\n" "total" = "📊 Всього: ↑↓{{ .UpDown }} / {{ .Total }}\r\n" "TGUser" = "👤 Користувач Telegram: {{ .TelegramID }}\r\n" "exhaustedMsg" = "🚨 Вичерпано {{ .Type }}:\r\n" "exhaustedCount" = "🚨 Вичерпано кількість {{ .Type }} count:\r\n" "onlinesCount" = "🌐 Онлайн-клієнти: {{ .Count }}\r\n" "disabled" = "🛑 Вимкнено: {{ .Disabled }}\r\n" "depleteSoon" = "🔜 Скоро вичерпається: {{ .Deplete }}\r\n\r\n" "backupTime" = "🗄 Час резервного копіювання: {{ .Time }}\r\n" "refreshedOn" = "\r\n📋🔄 Оновлено: {{ .Time }}\r\n\r\n" "yes" = "✅ Так" "no" = "❌ Ні" "received_id" = "🔑📥 ID оновлено." "received_password" = "🔑📥 Пароль оновлено." "received_email" = "📧📥 Електронна пошта оновлена." "received_comment" = "💬📥 Коментар оновлено." "id_prompt" = "🔑 Стандартний ID: {{ .ClientId }}\n\nВведіть ваш ID." "pass_prompt" = "🔑 Стандартний пароль: {{ .ClientPassword }}\n\nВведіть ваш пароль." "email_prompt" = "📧 Стандартний email: {{ .ClientEmail }}\n\nВведіть ваш email." "comment_prompt" = "💬 Стандартний коментар: {{ .ClientComment }}\n\nВведіть ваш коментар." "inbound_client_data_id" = "🔄 Вхід: {{ .InboundRemark }}\n\n🔑 ID: {{ .ClientId }}\n📧 Електронна пошта: {{ .ClientEmail }}\n📊 Трафік: {{ .ClientTraffic }}\n📅 Дата завершення: {{ .ClientExp }}\n🌐 Обмеження IP: {{ .IpLimit }}\n💬 Коментар: {{ .ClientComment }}\n\nТепер ви можете додати клієнта до вхідного з'єднання!" "inbound_client_data_pass" = "🔄 Вхід: {{ .InboundRemark }}\n\n🔑 Пароль: {{ .ClientPass }}\n📧 Електронна пошта: {{ .ClientEmail }}\n📊 Трафік: {{ .ClientTraffic }}\n📅 Дата завершення: {{ .ClientExp }}\n🌐 Обмеження IP: {{ .IpLimit }}\n💬 Коментар: {{ .ClientComment }}\n\nТепер ви можете додати клієнта до вхідного з'єднання!" "cancel" = "❌ Процес скасовано! \n\nВи можете знову розпочати, використовуючи /start у будь-який час. 🔄" "error_add_client" = "⚠️ Помилка:\n\n {{ .error }}" "using_default_value" = "Гаразд, залишу значення за замовчуванням. 😊" "incorrect_input" = "Ваш ввід невірний.\nФрази повинні бути без пробілів.\nПравильний приклад: aaaaaa\nНеправильний приклад: aaa aaa 🚫" "AreYouSure" = "Ви впевнені? 🤔" "SuccessResetTraffic" = "📧 Електронна пошта: {{ .ClientEmail }}\n🏁 Результат: ✅ Успішно" "FailedResetTraffic" = "📧 Електронна пошта: {{ .ClientEmail }}\n🏁 Результат: ❌ Невдача \n\n🛠️ Помилка: [ {{ .ErrorMessage }} ]" "FinishProcess" = "🔚 Процес скидання трафіку завершено для всіх клієнтів." [tgbot.buttons] "closeKeyboard" = "❌ Закрити клавіатуру" "cancel" = "❌ Скасувати" "cancelReset" = "❌ Скасувати скидання" "cancelIpLimit" = "❌ Скасувати обмеження IP" "confirmResetTraffic" = "✅ Підтвердити скидання трафіку?" "confirmClearIps" = "✅ Підтвердити очищення IP-адрес?" "confirmRemoveTGUser" = "✅ Підтвердити видалення користувача Telegram?" "confirmToggle" = "✅ Підтвердити ввімкнути/вимкнути користувача?" "dbBackup" = "Отримати резервну копію БД" "serverUsage" = "Використання сервера" "getInbounds" = "Отримати вхідні" "depleteSoon" = "Скоро вичерпати" "clientUsage" = "Отримати використання" "onlines" = "Онлайн-клієнти" "commands" = "Команди" "refresh" = "🔄 Оновити" "clearIPs" = "❌ Очистити IP-адреси" "removeTGUser" = "❌ Видалити користувача Telegram" "selectTGUser" = "👤 Виберіть користувача Telegram" "selectOneTGUser" = "👤 Виберіть користувача Telegram:" "resetTraffic" = "📈 Скинути трафік" "resetExpire" = "📅 Змінити термін дії" "ipLog" = "🔢 IP журнал" "ipLimit" = "🔢 IP Ліміт" "setTGUser" = "👤 Встановити користувача Telegram" "toggle" = "🔘 Увімкнути / Вимкнути" "custom" = "🔢 Custom" "confirmNumber" = "✅ Підтвердити: {{ .Num }}" "confirmNumberAdd" = "✅ Підтвердити додавання: {{ .Num }}" "limitTraffic" = "🚧 Ліміт трафіку" "getBanLogs" = "Отримати журнали заборон" "allClients" = "Всі Клієнти" "addClient" = "Додати клієнта" "submitDisable" = "Надіслати як вимкнено ☑️" "submitEnable" = "Надіслати як увімкнено ✅" "use_default" = "🏷️ Використати типове" "change_id" = "⚙️🔑 ID" "change_password" = "⚙️🔑 Пароль" "change_email" = "⚙️📧 Електронна пошта" "change_comment" = "⚙️💬 Коментар" "ResetAllTraffics" = "Скинути весь трафік" "SortedTrafficUsageReport" = "Відсортований звіт про використання трафіку" [tgbot.answers] "successfulOperation" = "✅ Операція успішна!" "errorOperation" = "❗ Помилка в роботі." "getInboundsFailed" = "❌ Не вдалося отримати вхідні повідомлення." "getClientsFailed" = "❌ Не вдалося отримати клієнтів." "canceled" = "❌ {{ .Email }}: Операцію скасовано." "clientRefreshSuccess" = "✅ {{ .Email }}: Клієнт успішно оновлено." "IpRefreshSuccess" = "✅ {{ .Email }}: IP-адреси успішно оновлено." "TGIdRefreshSuccess" = "✅ {{ .Email }}: Користувач Telegram клієнта успішно оновлено." "resetTrafficSuccess" = "✅ {{ .Email }}: Трафік скинуто успішно." "setTrafficLimitSuccess" = "✅ {{ .Email }}: Ліміт трафіку успішно збережено." "expireResetSuccess" = "✅ {{ .Email }}: Успішно скинуто дні закінчення терміну дії." "resetIpSuccess" = "✅ {{ .Email }}: IP обмеження {{ .Count }} успішно збережено." "clearIpSuccess" = "✅ {{ .Email }}: IP успішно очищено." "getIpLog" = "✅ {{ .Email }}: Отримати IP-журнал." "getUserInfo" = "✅ {{ .Email }}: Отримати інформацію про користувача Telegram." "removedTGUserSuccess" = "✅ {{ .Email }}: Користувача Telegram видалено успішно." "enableSuccess" = "✅ {{ .Email }}: Увімкнути успішно." "disableSuccess" = "✅ {{ .Email }}: Успішно вимкнено." "askToAddUserId" = "Вашу конфігурацію не знайдено!\r\nБудь ласка, попросіть свого адміністратора використовувати ваш ідентифікатор Telegram у вашій конфігурації.\r\n\r\nВаш ідентифікатор користувача: {{ .TgUserID }}" "chooseClient" = "Виберіть клієнта для Вхідного {{ .Inbound }}" "chooseInbound" = "Виберіть Вхідний" ================================================ FILE: web/translation/translate.vi_VN.toml ================================================ "username" = "Tên người dùng" "password" = "Mật khẩu" "login" = "Đăng nhập" "confirm" = "Xác nhận" "cancel" = "Hủy bỏ" "close" = "Đóng" "create" = "Tạo" "update" = "Cập nhật" "copy" = "Sao chép" "copied" = "Đã sao chép" "download" = "Tải xuống" "remark" = "Ghi chú" "enable" = "Kích hoạt" "protocol" = "Giao thức" "search" = "Tìm kiếm" "filter" = "Bộ lọc" "loading" = "Đang tải" "second" = "Giây" "minute" = "Phút" "hour" = "Giờ" "day" = "Ngày" "check" = "Kiểm tra" "indefinite" = "Không xác định" "unlimited" = "Không giới hạn" "none" = "None" "qrCode" = "Mã QR" "info" = "Thông tin thêm" "edit" = "Chỉnh sửa" "delete" = "Xóa" "reset" = "Đặt lại" "noData" = "Không có dữ liệu." "copySuccess" = "Đã sao chép thành công" "sure" = "Chắc chắn" "encryption" = "Mã hóa" "useIPv4ForHost" = "Sử dụng IPv4 cho máy chủ" "transmission" = "Truyền tải" "host" = "Máy chủ" "path" = "Đường dẫn" "camouflage" = "Ngụy trang" "status" = "Trạng thái" "enabled" = "Đã kích hoạt" "disabled" = "Đã tắt" "depleted" = "Depleted" "depletingSoon" = "Depleting..." "offline" = "Ngoại tuyến" "online" = "Trực tuyến" "domainName" = "Tên miền" "monitor" = "Listening IP" "certificate" = "Chứng chỉ số" "fail" = "Thất bại" "comment" = "Bình luận" "success" = "Thành công" "lastOnline" = "Lần online gần nhất" "getVersion" = "Lấy phiên bản" "install" = "Cài đặt" "clients" = "Các khách hàng" "usage" = "Sử dụng" "twoFactorCode" = "Mã" "remained" = "Còn lại" "security" = "Bảo vệ" "secAlertTitle" = "Cảnh báo an ninh-Tiếng Việt by Ohoang7" "secAlertSsl" = "Kết nối này không an toàn; Vui lòng không nhập thông tin nhạy cảm cho đến khi TLS được kích hoạt để bảo vệ dữ liệu của Bạn" "secAlertConf" = "Một số cài đặt có thể dễ bị tấn công. Đề xuất tăng cường các giao thức bảo mật để ngăn chặn các vi phạm tiềm ẩn." "secAlertSSL" = "Bảng điều khiển thiếu kết nối an toàn. Vui lòng cài đặt chứng chỉ TLS để bảo vệ dữ liệu." "secAlertPanelPort" = "Cổng mặc định của bảng điều khiển có thể dễ bị tấn công. Vui lòng cấu hình một cổng ngẫu nhiên hoặc cụ thể." "secAlertPanelURI" = "Đường dẫn URI mặc định của bảng điều khiển không an toàn. Vui lòng cấu hình một đường dẫn URI phức tạp." "secAlertSubURI" = "Đường dẫn URI mặc định của đăng ký không an toàn. Vui lòng cấu hình một đường dẫn URI phức tạp." "secAlertSubJsonURI" = "Đường dẫn URI JSON mặc định của đăng ký không an toàn. Vui lòng cấu hình một đường dẫn URI phức tạp." "emptyDnsDesc" = "Không có máy chủ DNS nào được thêm." "emptyFakeDnsDesc" = "Không có máy chủ Fake DNS nào được thêm." "emptyBalancersDesc" = "Không có bộ cân bằng tải nào được thêm." "emptyReverseDesc" = "Không có proxy ngược nào được thêm." "somethingWentWrong" = "Đã xảy ra lỗi" [subscription] "title" = "Thông tin đăng ký" "subId" = "ID đăng ký" "status" = "Trạng thái" "downloaded" = "Đã tải xuống" "uploaded" = "Đã tải lên" "expiry" = "Hết hạn" "totalQuota" = "Tổng hạn mức" "individualLinks" = "Liên kết riêng lẻ" "active" = "Hoạt động" "inactive" = "Không hoạt động" "unlimited" = "Không giới hạn" "noExpiry" = "Không hết hạn" [menu] "theme" = "Chủ đề" "dark" = "Tối" "ultraDark" = "Siêu tối" "dashboard" = "Trạng thái hệ thống" "inbounds" = "Đầu vào khách hàng" "settings" = "Cài đặt bảng điều khiển" "logout" = "Đăng xuất" "xray" = "Cài đặt Xray" "link" = "Quản lý" [pages.login] "hello" = "Xin chào" "title" = "Chào mừng" "loginAgain" = "Thời hạn đăng nhập đã hết. Vui lòng đăng nhập lại." [pages.login.toasts] "invalidFormData" = "Dạng dữ liệu nhập không hợp lệ." "emptyUsername" = "Vui lòng nhập tên người dùng." "emptyPassword" = "Vui lòng nhập mật khẩu." "wrongUsernameOrPassword" = "Tên người dùng, mật khẩu hoặc mã xác thực hai yếu tố không hợp lệ." "successLogin" = "Bạn đã đăng nhập vào tài khoản thành công." [pages.index] "title" = "Trạng thái hệ thống" "cpu" = "CPU" "logicalProcessors" = "Bộ xử lý logic" "frequency" = "Tần số" "swap" = "Swap" "storage" = "Lưu trữ" "memory" = "RAM" "threads" = "Luồng" "xrayStatus" = "Xray" "stopXray" = "Dừng lại" "restartXray" = "Khởi động lại" "xraySwitch" = "Phiên bản" "xraySwitchClick" = "Chọn phiên bản mà bạn muốn chuyển đổi sang." "xraySwitchClickDesk" = "Hãy lựa chọn thận trọng, vì các phiên bản cũ có thể không tương thích với các cấu hình hiện tại." "xrayStatusUnknown" = "Không xác định" "xrayStatusRunning" = "Đang chạy" "xrayStatusStop" = "Dừng" "xrayStatusError" = "Lỗi" "xrayErrorPopoverTitle" = "Đã xảy ra lỗi khi chạy Xray" "operationHours" = "Thời gian hoạt động" "systemLoad" = "Tải hệ thống" "systemLoadDesc" = "trung bình tải hệ thống trong 1, 5 và 15 phút qua" "connectionCount" = "Số lượng kết nối" "ipAddresses" = "Địa chỉ IP" "toggleIpVisibility" = "Chuyển đổi hiển thị IP" "overallSpeed" = "Tốc độ tổng thể" "upload" = "Tải lên" "download" = "Tải xuống" "totalData" = "Tổng dữ liệu" "sent" = "Đã gửi" "received" = "Đã nhận" "documentation" = "Tài liệu" "xraySwitchVersionDialog" = "Bạn có chắc chắn muốn thay đổi phiên bản Xray không?" "xraySwitchVersionDialogDesc" = "Hành động này sẽ thay đổi phiên bản Xray thành #version#." "xraySwitchVersionPopover" = "Xray đã được cập nhật thành công" "geofileUpdateDialog" = "Bạn có chắc chắn muốn cập nhật geofile không?" "geofileUpdateDialogDesc" = "Hành động này sẽ cập nhật tệp #filename#." "geofilesUpdateDialogDesc" = "Thao tác này sẽ cập nhật tất cả các tập tin." "geofilesUpdateAll" = "Cập nhật tất cả" "geofileUpdatePopover" = "Geofile đã được cập nhật thành công" "dontRefresh" = "Đang tiến hành cài đặt, vui lòng không làm mới trang này." "logs" = "Nhật ký" "config" = "Cấu hình" "backup" = "Sao lưu" "backupTitle" = "Sao lưu & Khôi phục Cơ sở dữ liệu" "exportDatabase" = "Sao lưu" "exportDatabaseDesc" = "Nhấp để tải xuống tệp .db chứa bản sao lưu cơ sở dữ liệu hiện tại của bạn vào thiết bị." "importDatabase" = "Khôi phục" "importDatabaseDesc" = "Nhấp để chọn và tải lên tệp .db từ thiết bị của bạn để khôi phục cơ sở dữ liệu từ bản sao lưu." "importDatabaseSuccess" = "Đã nhập cơ sở dữ liệu thành công" "importDatabaseError" = "Lỗi xảy ra khi nhập cơ sở dữ liệu" "readDatabaseError" = "Lỗi xảy ra khi đọc cơ sở dữ liệu" "getDatabaseError" = "Lỗi xảy ra khi truy xuất cơ sở dữ liệu" "getConfigError" = "Lỗi xảy ra khi truy xuất tệp cấu hình" [pages.inbounds] "allTimeTraffic" = "Tổng Lưu Lượng" "allTimeTrafficUsage" = "Tổng mức sử dụng mọi lúc" "title" = "Điểm vào (Inbounds)" "totalDownUp" = "Tổng tải lên/tải xuống" "totalUsage" = "Tổng sử dụng" "inboundCount" = "Số lượng điểm vào" "operate" = "Thao tác" "enable" = "Kích hoạt" "remark" = "Chú thích" "protocol" = "Giao thức" "port" = "Cổng" "portMap" = "Cổng tạo" "traffic" = "Lưu lượng" "details" = "Chi tiết" "transportConfig" = "Giao vận" "expireDate" = "Ngày hết hạn" "createdAt" = "Tạo lúc" "updatedAt" = "Cập nhật" "resetTraffic" = "Đặt lại lưu lượng" "addInbound" = "Thêm điểm vào" "generalActions" = "Hành động chung" "autoRefresh" = "Tự động làm mới" "autoRefreshInterval" = "Khoảng thời gian" "modifyInbound" = "Chỉnh sửa điểm vào (Inbound)" "deleteInbound" = "Xóa điểm vào (Inbound)" "deleteInboundContent" = "Xác nhận xóa điểm vào? (Inbound)" "deleteClient" = "Xóa người dùng" "deleteClientContent" = "Bạn có chắc chắn muốn xóa người dùng không?" "resetTrafficContent" = "Xác nhận đặt lại lưu lượng?" "copyLink" = "Sao chép liên kết" "address" = "Địa chỉ" "network" = "Mạng" "destinationPort" = "Cổng đích" "targetAddress" = "Địa chỉ mục tiêu" "monitorDesc" = "Mặc định để trống" "meansNoLimit" = "= Không giới hạn (đơn vị: GB)" "totalFlow" = "Tổng lưu lượng" "leaveBlankToNeverExpire" = "Để trống để không bao giờ hết hạn" "noRecommendKeepDefault" = "Không yêu cầu đặc biệt để giữ nguyên cài đặt mặc định" "certificatePath" = "Đường dẫn tập" "certificateContent" = "Nội dung tập" "publicKey" = "Khóa công khai" "privatekey" = "Khóa cá nhân" "clickOnQRcode" = "Nhấn vào Mã QR để sao chép" "client" = "Người dùng" "export" = "Xuất liên kết" "clone" = "Sao chép" "cloneInbound" = "Sao chép điểm vào (Inbound)" "cloneInboundContent" = "Tất cả cài đặt của điểm vào này, trừ Cổng, IP nghe và máy khách, sẽ được áp dụng cho bản sao." "cloneInboundOk" = "Sao chép" "resetAllTraffic" = "Đặt lại lưu lượng cho tất cả điểm vào" "resetAllTrafficTitle" = "Đặt lại lưu lượng cho tất cả điểm vào" "resetAllTrafficContent" = "Bạn có chắc chắn muốn đặt lại lưu lượng cho tất cả điểm vào không?" "resetInboundClientTraffics" = "Đặt lại lưu lượng toàn bộ người dùng của điểm vào" "resetInboundClientTrafficTitle" = "Đặt lại lưu lượng cho toàn bộ người dùng của điểm vào" "resetInboundClientTrafficContent" = "Bạn có chắc chắn muốn đặt lại tất cả lưu lượng cho các người dùng của điểm vào này không?" "resetAllClientTraffics" = "Đặt lại lưu lượng cho toàn bộ người dùng" "resetAllClientTrafficTitle" = "Đặt lại lưu lượng cho toàn bộ người dùng" "resetAllClientTrafficContent" = "Bạn có chắc chắn muốn đặt lại tất cả lưu lượng cho toàn bộ người dùng không?" "delDepletedClients" = "Xóa các người dùng đã cạn kiệt" "delDepletedClientsTitle" = "Xóa các người dùng đã cạn kiệt" "delDepletedClientsContent" = "Bạn có chắc chắn muốn xóa toàn bộ người dùng đã cạn kiệt không?" "email" = "Email" "emailDesc" = "Vui lòng cung cấp một địa chỉ email duy nhất." "IPLimit" = "Giới hạn IP" "IPLimitDesc" = "Vô hiệu hóa điểm vào nếu số lượng vượt quá giá trị đã nhập (nhập 0 để vô hiệu hóa giới hạn IP)." "IPLimitlog" = "Lịch sử IP" "IPLimitlogDesc" = "Lịch sử đăng nhập IP (trước khi kích hoạt điểm vào sau khi bị vô hiệu hóa bởi giới hạn IP, bạn nên xóa lịch sử)." "IPLimitlogclear" = "Xóa Lịch sử" "setDefaultCert" = "Đặt chứng chỉ từ bảng điều khiển" "telegramDesc" = "Vui lòng cung cấp ID Trò chuyện Telegram. (sử dụng lệnh '/id' trong bot) hoặc (@userinfobot)" "subscriptionDesc" = "Bạn có thể tìm liên kết gói đăng ký của mình trong Chi tiết, cũng như bạn có thể sử dụng cùng tên cho nhiều cấu hình khác nhau" "info" = "Thông tin" "same" = "Giống nhau" "inboundData" = "Dữ liệu gửi đến" "exportInbound" = "Xuất nhập khẩu" "import" = "Nhập" "importInbound" = "Nhập inbound" "periodicTrafficResetTitle" = "Đặt lại lưu lượng" "periodicTrafficResetDesc" = "Tự động đặt lại bộ đếm lưu lượng theo khoảng thời gian xác định" "lastReset" = "Đặt lại lần cuối" [pages.client] "add" = "Thêm người dùng" "edit" = "Chỉnh sửa người dùng" "submitAdd" = "Thêm" "submitEdit" = "Lưu thay đổi" "clientCount" = "Số lượng người dùng" "bulk" = "Thêm hàng loạt" "method" = "Phương pháp" "first" = "Đầu tiên" "last" = "Cuối cùng" "prefix" = "Tiền tố" "postfix" = "Hậu tố" "delayedStart" = "Bắt đầu ở Lần Đầu" "expireDays" = "Khoảng thời gian" "days" = "ngày" "renew" = "Tự động gia hạn" "renewDesc" = "Tự động gia hạn sau khi hết hạn. (0 = tắt)(đơn vị: ngày)" [pages.inbounds.periodicTrafficReset] "never" = "Không bao giờ" "daily" = "Hàng ngày" "weekly" = "Hàng tuần" "monthly" = "Hàng tháng" [pages.inbounds.toasts] "obtain" = "Nhận" "updateSuccess" = "Cập nhật thành công" "logCleanSuccess" = "Đã xóa nhật ký" "inboundsUpdateSuccess" = "Đã cập nhật thành công các kết nối inbound" "inboundUpdateSuccess" = "Đã cập nhật thành công kết nối inbound" "inboundCreateSuccess" = "Đã tạo thành công kết nối inbound" "inboundDeleteSuccess" = "Đã xóa thành công kết nối inbound" "inboundClientAddSuccess" = "Đã thêm client inbound" "inboundClientDeleteSuccess" = "Đã xóa client inbound" "inboundClientUpdateSuccess" = "Đã cập nhật client inbound" "delDepletedClientsSuccess" = "Đã xóa tất cả client hết hạn" "resetAllClientTrafficSuccess" = "Đã đặt lại toàn bộ lưu lượng client" "resetAllTrafficSuccess" = "Đã đặt lại toàn bộ lưu lượng" "resetInboundClientTrafficSuccess" = "Đã đặt lại lưu lượng" "trafficGetError" = "Lỗi khi lấy thông tin lưu lượng" "getNewX25519CertError" = "Lỗi khi lấy chứng chỉ X25519." "getNewmldsa65Error" = "Lỗi khi lấy chứng chỉ mldsa65." "getNewVlessEncError" = "Lỗi khi lấy chứng chỉ VlessEnc." [pages.inbounds.stream.general] "request" = "Lời yêu cầu" "response" = "Phản ứng" "name" = "Tên" "value" = "Giá trị" [pages.inbounds.stream.tcp] "version" = "Phiên bản" "method" = "Phương pháp" "path" = "Đường dẫn" "status" = "Trạng thái" "statusDescription" = "Tình trạng Mô tả" "requestHeader" = "Header yêu cầu" "responseHeader" = "Header phản hồi" [pages.settings] "title" = "Cài đặt" "save" = "Lưu" "infoDesc" = "Mọi thay đổi được thực hiện ở đây cần phải được lưu. Vui lòng khởi động lại bảng điều khiển để áp dụng các thay đổi." "restartPanel" = "Khởi động lại bảng điều khiển" "restartPanelDesc" = "Bạn có chắc chắn muốn khởi động lại bảng điều khiển? Nhấn OK để khởi động lại sau 3 giây. Nếu bạn không thể truy cập bảng điều khiển sau khi khởi động lại, vui lòng xem thông tin nhật ký của bảng điều khiển trên máy chủ." "restartPanelSuccess" = "Đã khởi động lại bảng điều khiển thành công" "actions" = "Hành động" "resetDefaultConfig" = "Đặt lại cấu hình mặc định" "panelSettings" = "Bảng điều khiển" "securitySettings" = "Bảo mật" "TGBotSettings" = "Bot Telegram" "panelListeningIP" = "IP Nghe của bảng điều khiển" "panelListeningIPDesc" = "Mặc định để trống để nghe tất cả các IP." "panelListeningDomain" = "Tên miền của nghe bảng điều khiển" "panelListeningDomainDesc" = "Mặc định để trống để nghe tất cả các tên miền và IP" "panelPort" = "Cổng bảng điều khiển" "panelPortDesc" = "Cổng được sử dụng để kết nối với bảng điều khiển này" "publicKeyPath" = "Đường dẫn file chứng chỉ bảng điều khiển" "publicKeyPathDesc" = "Điền vào đường dẫn đầy đủ (bắt đầu từ '/')" "privateKeyPath" = "Đường dẫn file khóa của chứng chỉ bảng điều khiển" "privateKeyPathDesc" = "Điền vào đường dẫn đầy đủ (bắt đầu từ '/')" "panelUrlPath" = "Đường dẫn gốc URL bảng điều khiển" "panelUrlPathDesc" = "Phải bắt đầu và kết thúc bằng '/'" "pageSize" = "Kích thước phân trang" "pageSizeDesc" = "Xác định kích thước trang cho bảng gửi đến. Đặt 0 để tắt" "remarkModel" = "Ghi chú mô hình và ký tự phân tách" "datepicker" = "Kiểu lịch" "datepickerPlaceholder" = "Chọn ngày" "datepickerDescription" = "Tác vụ chạy theo lịch trình sẽ chạy theo kiểu lịch này." "sampleRemark" = "Nhận xét mẫu" "oldUsername" = "Tên người dùng hiện tại" "currentPassword" = "Mật khẩu hiện tại" "newUsername" = "Tên người dùng mới" "newPassword" = "Mật khẩu mới" "telegramBotEnable" = "Bật Bot Telegram" "telegramBotEnableDesc" = "Kết nối với các tính năng của bảng điều khiển này thông qua bot Telegram" "telegramToken" = "Token Telegram" "telegramTokenDesc" = "Bạn phải nhận token từ quản lý bot Telegram @botfather" "telegramProxy" = "Socks5 Proxy" "telegramProxyDesc" = "Nếu bạn cần socks5 proxy để kết nối với Telegram. Điều chỉnh cài đặt của nó theo hướng dẫn." "telegramAPIServer" = "Telegram API Server" "telegramAPIServerDesc" = "Máy chủ API Telegram để sử dụng. Để trống để sử dụng máy chủ mặc định." "telegramChatId" = "Chat ID Telegram của quản trị viên" "telegramChatIdDesc" = "Nhiều Chat ID phân tách bằng dấu phẩy. Sử dụng @userinfobot hoặc sử dụng lệnh '/id' trong bot để lấy Chat ID của bạn." "telegramNotifyTime" = "Thời gian thông báo của bot Telegram" "telegramNotifyTimeDesc" = "Sử dụng định dạng thời gian Crontab." "tgNotifyBackup" = "Sao lưu Cơ sở dữ liệu" "tgNotifyBackupDesc" = "Bao gồm tệp sao lưu cơ sở dữ liệu với thông báo báo cáo." "tgNotifyLogin" = "Thông báo Đăng nhập" "tgNotifyLoginDesc" = "Hiển thị tên người dùng, địa chỉ IP và thời gian khi ai đó cố gắng đăng nhập vào bảng điều khiển của bạn." "sessionMaxAge" = "Thời gian tối đa của phiên" "sessionMaxAgeDesc" = "Thời gian của phiên đăng nhập (đơn vị: phút)" "expireTimeDiff" = "Ngưỡng hết hạn cho thông báo" "expireTimeDiffDesc" = "Nhận thông báo về việc hết hạn tài khoản trước ngưỡng này (đơn vị: ngày)" "trafficDiff" = "Ngưỡng lưu lượng cho thông báo" "trafficDiffDesc" = "Nhận thông báo về việc cạn kiệt lưu lượng trước khi đạt đến ngưỡng này (đơn vị: GB)" "tgNotifyCpu" = "Ngưỡng cảnh báo tỷ lệ CPU" "tgNotifyCpuDesc" = "Nhận thông báo nếu tỷ lệ sử dụng CPU vượt quá ngưỡng này (đơn vị: %)" "timeZone" = "Múi giờ" "timeZoneDesc" = "Các tác vụ được lên lịch chạy theo thời gian trong múi giờ này." "subSettings" = "Gói đăng ký" "subEnable" = "Bật dịch vụ" "subEnableDesc" = "Tính năng gói đăng ký với cấu hình riêng" "subJsonEnable" = "Bật/Tắt điểm cuối đăng ký JSON độc lập." "subTitle" = "Tiêu đề Đăng ký" "subTitleDesc" = "Tiêu đề hiển thị trong ứng dụng VPN" "subSupportUrl" = "URL Hỗ trợ" "subSupportUrlDesc" = "Liên kết hỗ trợ kỹ thuật hiển thị trong ứng dụng VPN" "subProfileUrl" = "URL Hồ sơ" "subProfileUrlDesc" = "Liên kết đến trang web của bạn hiển thị trong ứng dụng VPN" "subAnnounce" = "Thông báo" "subAnnounceDesc" = "Văn bản thông báo hiển thị trong ứng dụng VPN" "subEnableRouting" = "Bật định tuyến" "subEnableRoutingDesc" = "Cài đặt toàn cục để bật định tuyến trong ứng dụng khách VPN. (Chỉ dành cho Happ)" "subRoutingRules" = "Quy tắc định tuyến" "subRoutingRulesDesc" = "Quy tắc định tuyến toàn cầu cho client VPN. (Chỉ dành cho Happ)" "subListen" = "Listening IP" "subListenDesc" = "Mặc định để trống để nghe tất cả các IP" "subPort" = "Cổng gói đăng ký" "subPortDesc" = "Số cổng dịch vụ đăng ký phải chưa được sử dụng trên máy chủ" "subCertPath" = "Đường dẫn file chứng chỉ gói đăng ký" "subCertPathDesc" = "Điền vào đường dẫn đầy đủ (bắt đầu với '/')" "subKeyPath" = "Đường dẫn file khóa của chứng chỉ gói đăng ký" "subKeyPathDesc" = "Điền vào đường dẫn đầy đủ (bắt đầu với '/')" "subPath" = "Đường dẫn gốc URL gói đăng ký" "subPathDesc" = "Phải bắt đầu và kết thúc bằng '/'" "subDomain" = "Tên miền con" "subDomainDesc" = "Mặc định để trống để nghe tất cả các tên miền và IP" "subUpdates" = "Khoảng thời gian cập nhật gói đăng ký" "subUpdatesDesc" = "Số giờ giữa các cập nhật trong ứng dụng khách" "subEncrypt" = "Mã hóa cấu hình" "subEncryptDesc" = "Mã hóa các cấu hình được trả về trong gói đăng ký" "subShowInfo" = "Hiển thị thông tin sử dụng" "subShowInfoDesc" = "Hiển thị lưu lượng truy cập còn lại và ngày sau tên cấu hình" "subURI" = "URI proxy trung gian" "subURIDesc" = "Thay đổi URI cơ sở của URL gói đăng ký để sử dụng cho proxy trung gian" "externalTrafficInformEnable" = "Thông báo giao thông bên ngoài" "externalTrafficInformEnableDesc" = "Thông báo cho API bên ngoài về mọi cập nhật lưu lượng truy cập." "externalTrafficInformURI" = "URI thông báo lưu lượng truy cập bên ngoài" "externalTrafficInformURIDesc" = "Cập nhật lưu lượng truy cập được gửi tới URI này." "fragment" = "Sự phân mảnh" "fragmentDesc" = "Kích hoạt phân mảnh cho gói TLS hello" "fragmentSett" = "Cài đặt phân mảnh" "noisesDesc" = "Bật Noises." "noisesSett" = "Cài đặt Noises" "mux" = "Mux" "muxDesc" = "Truyền nhiều luồng dữ liệu độc lập trong luồng dữ liệu đã thiết lập." "muxSett" = "Mux Cài đặt" "direct" = "Kết nối trực tiếp" "directDesc" = "Trực tiếp thiết lập kết nối với tên miền hoặc dải IP của một quốc gia cụ thể." "notifications" = "Thông báo" "certs" = "Chứng chỉ" "externalTraffic" = "Lưu lượng bên ngoài" "dateAndTime" = "Ngày và giờ" "proxyAndServer" = "Proxy và máy chủ" "intervals" = "Khoảng thời gian" "information" = "Thông tin" "language" = "Ngôn ngữ" "telegramBotLanguage" = "Ngôn ngữ của Bot Telegram" [pages.xray] "title" = "Cài đặt Xray" "save" = "Lưu cài đặt" "restart" = "Khởi động lại Xray" "restartSuccess" = "Đã khởi động lại Xray thành công" "stopSuccess" = "Xray đã được dừng thành công" "restartError" = "Đã xảy ra lỗi khi khởi động lại Xray." "stopError" = "Đã xảy ra lỗi khi dừng Xray." "basicTemplate" = "Mẫu Cơ bản" "advancedTemplate" = "Mẫu Nâng cao" "generalConfigs" = "Cấu hình Chung" "generalConfigsDesc" = "Những tùy chọn này sẽ cung cấp điều chỉnh tổng quát." "logConfigs" = "Nhật ký" "logConfigsDesc" = "Nhật ký có thể ảnh hưởng đến hiệu suất máy chủ của bạn. Bạn chỉ nên kích hoạt nó một cách khôn ngoan trong trường hợp bạn cần" "blockConfigsDesc" = "Những tùy chọn này sẽ ngăn người dùng kết nối đến các giao thức và trang web cụ thể." "basicRouting" = "Định tuyến Cơ bản" "blockConnectionsConfigsDesc" = "Các tùy chọn này sẽ chặn lưu lượng truy cập dựa trên quốc gia được yêu cầu cụ thể." "directConnectionsConfigsDesc" = "Kết nối trực tiếp đảm bảo rằng lưu lượng truy cập cụ thể không được định tuyến qua máy chủ khác." "blockips" = "Chặn IP" "blockdomains" = "Chặn Tên Miền" "directips" = "IP Trực Tiếp" "directdomains" = "Tên Miền Trực Tiếp" "ipv4Routing" = "Định tuyến IPv4" "ipv4RoutingDesc" = "Những tùy chọn này sẽ chỉ định kết nối đến các tên miền mục tiêu qua IPv4." "warpRouting" = "Định tuyến WARP" "warpRoutingDesc" = "Cảnh báo: Trước khi sử dụng những tùy chọn này, hãy cài đặt WARP ở chế độ proxy socks5 trên máy chủ của bạn bằng cách làm theo các bước trên GitHub của bảng điều khiển. WARP sẽ định tuyến lưu lượng đến các trang web qua máy chủ Cloudflare." "Template" = "Mẫu Cấu hình Xray" "TemplateDesc" = "Tạo tệp cấu hình Xray cuối cùng dựa trên mẫu này." "FreedomStrategy" = "Cấu hình Chiến lược cho Giao thức Freedom" "FreedomStrategyDesc" = "Đặt chiến lược đầu ra của mạng trong Giao thức Freedom." "RoutingStrategy" = "Cấu hình Chiến lược Định tuyến Tên miền" "RoutingStrategyDesc" = "Đặt chiến lược định tuyến tổng thể cho việc giải quyết DNS." "outboundTestUrl" = "URL kiểm tra outbound" "outboundTestUrlDesc" = "URL dùng khi kiểm tra kết nối outbound" "Torrent" = "Cấu hình sử dụng BitTorrent" "Inbounds" = "Đầu vào" "InboundsDesc" = "Thay đổi mẫu cấu hình để chấp nhận các máy khách cụ thể." "Outbounds" = "Đầu ra" "Balancers" = "Cân bằng" "OutboundsDesc" = "Thay đổi mẫu cấu hình để xác định các cách ra đi cho máy chủ này." "Routings" = "Quy tắc định tuyến" "RoutingsDesc" = "Mức độ ưu tiên của mỗi quy tắc đều quan trọng!" "completeTemplate" = "All" "logLevel" = "Mức đăng nhập" "logLevelDesc" = "Cấp độ nhật ký cho nhật ký lỗi, cho biết thông tin cần được ghi lại." "accessLog" = "Nhật ký truy cập" "accessLogDesc" = "Đường dẫn tệp cho nhật ký truy cập. Nhật ký truy cập bị vô hiệu hóa có giá trị đặc biệt 'không'" "errorLog" = "Nhật ký lỗi" "errorLogDesc" = "Đường dẫn tệp cho nhật ký lỗi. Nhật ký lỗi bị vô hiệu hóa có giá trị đặc biệt 'không'" "dnsLog" = "Nhật ký DNS" "dnsLogDesc" = "Có bật nhật ký truy vấn DNS không" "maskAddress" = "Ẩn Địa Chỉ" "maskAddressDesc" = "Mặt nạ địa chỉ IP, khi được bật, sẽ tự động thay thế địa chỉ IP xuất hiện trong nhật ký." "statistics" = "Thống kê" "statsInboundUplink" = "Thống kê tải lên đầu vào" "statsInboundUplinkDesc" = "Kích hoạt thu thập thống kê cho lưu lượng tải lên của tất cả các proxy đầu vào." "statsInboundDownlink" = "Thống kê tải xuống đầu vào" "statsInboundDownlinkDesc" = "Kích hoạt thu thập thống kê cho lưu lượng tải xuống của tất cả các proxy đầu vào." "statsOutboundUplink" = "Thống kê tải lên đầu ra" "statsOutboundUplinkDesc" = "Kích hoạt thu thập thống kê cho lưu lượng tải lên của tất cả các proxy đầu ra." "statsOutboundDownlink" = "Thống kê tải xuống đầu ra" "statsOutboundDownlinkDesc" = "Kích hoạt thu thập thống kê cho lưu lượng tải xuống của tất cả các proxy đầu ra." [pages.xray.rules] "first" = "Đầu tiên" "last" = "Cuối cùng" "up" = "Lên" "down" = "Xuống" "source" = "Nguồn" "dest" = "Đích" "inbound" = "Vào" "outbound" = "Ra" "balancer" = "Cân bằng" "info" = "Thông tin" "add" = "Thêm quy tắc" "edit" = "Chỉnh sửa quy tắc" "useComma" = "Các mục được phân tách bằng dấu phẩy" [pages.xray.outbound] "addOutbound" = "Thêm thư đi" "addReverse" = "Thêm đảo ngược" "editOutbound" = "Chỉnh sửa gửi đi" "editReverse" = "Chỉnh sửa ngược lại" "tag" = "Thẻ" "tagDesc" = "thẻ duy nhất" "address" = "Địa chỉ" "reverse" = "Đảo ngược" "domain" = "Miền" "type" = "Loại" "bridge" = "Cầu" "portal" = "Cổng thông tin" "link" = "Liên kết" "intercon" = "Kết nối" "settings" = "cài đặt" "accountInfo" = "Thông tin tài khoản" "outboundStatus" = "Trạng thái đầu ra" "sendThrough" = "Gửi qua" "test" = "Kiểm tra" "testResult" = "Kết quả kiểm tra" "testing" = "Đang kiểm tra kết nối..." "testSuccess" = "Kiểm tra thành công" "testFailed" = "Kiểm tra thất bại" "testError" = "Không thể kiểm tra đầu ra" [pages.xray.balancer] "addBalancer" = "Thêm cân bằng" "editBalancer" = "Chỉnh sửa cân bằng" "balancerStrategy" = "Chiến lược" "balancerSelectors" = "Bộ chọn" "tag" = "Thẻ" "tagDesc" = "thẻ duy nhất" "balancerDesc" = "Không thể sử dụng balancerTag và outboundTag cùng một lúc. Nếu sử dụng cùng lúc thì chỉ outboundTag mới hoạt động." [pages.xray.wireguard] "secretKey" = "Khoá bí mật" "publicKey" = "Khóa công khai" "allowedIPs" = "IP được phép" "endpoint" = "Điểm cuối" "psk" = "Khóa chia sẻ" "domainStrategy" = "Chiến lược tên miền" [pages.xray.tun] "nameDesc" = "Tên của giao diện TUN. Giá trị mặc định là 'xray0'" "mtuDesc" = "Đơn vị Truyền Tối đa. Kích thước tối đa của các gói dữ liệu. Giá trị mặc định là 1500" "userLevel" = "Mức Người Dùng" "userLevelDesc" = "Tất cả các kết nối được thực hiện thông qua inbound này sẽ sử dụng mức người dùng này. Giá trị mặc định là 0" [pages.xray.dns] "enable" = "Kích hoạt DNS" "enableDesc" = "Kích hoạt máy chủ DNS tích hợp" "tag" = "Thẻ gửi đến DNS" "tagDesc" = "Thẻ này sẽ có sẵn dưới dạng thẻ Gửi đến trong quy tắc định tuyến." "clientIp" = "IP khách hàng" "clientIpDesc" = "Được sử dụng để thông báo cho máy chủ về vị trí IP được chỉ định trong các truy vấn DNS" "disableCache" = "Tắt bộ nhớ đệm" "disableCacheDesc" = "Tắt bộ nhớ đệm DNS" "disableFallback" = "Tắt Fallback" "disableFallbackDesc" = "Tắt các truy vấn DNS Fallback" "disableFallbackIfMatch" = "Tắt Fallback Nếu Khớp" "disableFallbackIfMatchDesc" = "Tắt các truy vấn DNS Fallback khi danh sách tên miền khớp của máy chủ DNS được kích hoạt" "enableParallelQuery" = "Bật Truy vấn Song song" "enableParallelQueryDesc" = "Bật truy vấn DNS song song đến nhiều máy chủ để phân giải nhanh hơn" "strategy" = "Chiến lược truy vấn" "strategyDesc" = "Chiến lược tổng thể để phân giải tên miền" "add" = "Thêm máy chủ" "edit" = "Chỉnh sửa máy chủ" "domains" = "Tên miền" "expectIPs" = "Các IP Dự Kiến" "unexpectIPs" = "IP không mong muốn" "useSystemHosts" = "Sử dụng Hosts hệ thống" "useSystemHostsDesc" = "Sử dụng file hosts từ hệ thống đã cài đặt" "usePreset" = "Dùng mẫu" "dnsPresetTitle" = "Mẫu DNS" "dnsPresetFamily" = "Gia đình" [pages.xray.fakedns] "add" = "Thêm DNS giả" "edit" = "Chỉnh sửa DNS giả" "ipPool" = "Mạng con nhóm IP" "poolSize" = "Kích thước bể bơi" [pages.settings.security] "admin" = "Thông tin đăng nhập quản trị viên" "twoFactor" = "Xác thực hai yếu tố" "twoFactorEnable" = "Bật 2FA" "twoFactorEnableDesc" = "Thêm một lớp bảo mật bổ sung để tăng cường an toàn." "twoFactorModalSetTitle" = "Bật xác thực hai yếu tố" "twoFactorModalDeleteTitle" = "Tắt xác thực hai yếu tố" "twoFactorModalSteps" = "Để thiết lập xác thực hai yếu tố, hãy thực hiện các bước sau:" "twoFactorModalFirstStep" = "1. Quét mã QR này trong ứng dụng xác thực hoặc sao chép mã token gần mã QR và dán vào ứng dụng" "twoFactorModalSecondStep" = "2. Nhập mã từ ứng dụng" "twoFactorModalRemoveStep" = "Nhập mã từ ứng dụng để xóa xác thực hai yếu tố." "twoFactorModalChangeCredentialsTitle" = "Thay đổi thông tin xác thực" "twoFactorModalChangeCredentialsStep" = "Nhập mã từ ứng dụng để thay đổi thông tin xác thực quản trị viên." "twoFactorModalSetSuccess" = "Xác thực hai yếu tố đã được thiết lập thành công" "twoFactorModalDeleteSuccess" = "Xác thực hai yếu tố đã được xóa thành công" "twoFactorModalError" = "Mã sai" [pages.settings.toasts] "modifySettings" = "Các tham số đã được thay đổi." "getSettings" = "Lỗi xảy ra khi truy xuất tham số." "modifyUserError" = "Đã xảy ra lỗi khi thay đổi thông tin đăng nhập quản trị viên." "modifyUser" = "Bạn đã thay đổi thông tin đăng nhập quản trị viên thành công." "originalUserPassIncorrect" = "Tên người dùng hoặc mật khẩu gốc không đúng" "userPassMustBeNotEmpty" = "Tên người dùng mới và mật khẩu mới không thể để trống" "getOutboundTrafficError" = "Lỗi khi lấy lưu lượng truy cập đi" "resetOutboundTrafficError" = "Lỗi khi đặt lại lưu lượng truy cập đi" [tgbot] "keyboardClosed" = "❌ Bàn phím đã đóng!" "noResult" = "❗ Không có kết quả!" "noQuery" = "❌ Không tìm thấy truy vấn! Vui lòng sử dụng lại lệnh!" "wentWrong" = "❌ Đã xảy ra lỗi!" "noIpRecord" = "❗ Không có bản ghi IP!" "noInbounds" = "❗ Không tìm thấy inbound!" "unlimited" = "♾ Không giới hạn (Đặt lại)" "add" = "Thêm" "month" = "Tháng" "months" = "Tháng" "day" = "Ngày" "days" = "Ngày" "hours" = "Giờ" "minutes" = "Phút" "unknown" = "Không xác định" "inbounds" = "Inbound" "clients" = "Client" "offline" = "🔴 Ngoại tuyến" "online" = "🟢 Trực tuyến" [tgbot.commands] "unknown" = "❗ Lệnh không rõ" "pleaseChoose" = "👇 Vui lòng chọn:\r\n" "help" = "🤖 Chào mừng bạn đến với bot này! Bot được thiết kế để cung cấp cho bạn dữ liệu cụ thể từ máy chủ và cho phép bạn thực hiện các thay đổi cần thiết.\r\n\r\n" "start" = "👋 Xin chào {{ .Firstname }}.\r\n" "welcome" = "🤖 Chào mừng đến với bot quản lý của {{ .Hostname }}.\r\n" "status" = "✅ Bot hoạt động bình thường!" "usage" = "❗ Vui lòng cung cấp văn bản để tìm kiếm!" "getID" = "🆔 ID của bạn: {{ .ID }}" "helpAdminCommands" = "Để khởi động lại Xray Core:\r\n/restart\r\n\r\nĐể tìm kiếm email của khách hàng:\r\n/usage [Email]\r\n\r\nĐể tìm kiếm các nhập (với số liệu thống kê của khách hàng):\r\n/inbound [Ghi chú]\r\n\r\nID Trò chuyện Telegram:\r\n/id" "helpClientCommands" = "Để tìm kiếm thống kê, sử dụng lệnh sau:\r\n/usage [Email]\r\n\r\nID Trò chuyện Telegram:\r\n/id" "restartUsage" = "\r\n\r\n/restart" "restartSuccess" = "✅ Hoạt động thành công!" "restartFailed" = "❗ Lỗi trong quá trình hoạt động.\r\n\r\nLỗi: {{ .Error }}." "xrayNotRunning" = "❗ Xray Core không chạy." "startDesc" = "Hiển thị menu chính" "helpDesc" = "Trợ giúp bot" "statusDesc" = "Kiểm tra trạng thái bot" "idDesc" = "Hiển thị ID Telegram của bạn" [tgbot.messages] "cpuThreshold" = "🔴 Sử dụng CPU {{ .Percent }}% vượt quá ngưỡng {{ .Threshold }}%" "selectUserFailed" = "❌ Lỗi khi chọn người dùng!" "userSaved" = "✅ Người dùng Telegram đã được lưu." "loginSuccess" = "✅ Đăng nhập thành công vào bảng điều khiển.\r\n" "loginFailed" = "❗️ Đăng nhập vào bảng điều khiển thất bại.\r\n" "2faFailed" = "Lỗi 2FA" "report" = "🕰 Báo cáo định kỳ: {{ .RunTime }}\r\n" "datetime" = "⏰ Ngày-Giờ: {{ .DateTime }}\r\n" "hostname" = "💻 Tên máy chủ: {{ .Hostname }}\r\n" "version" = "🚀 Phiên bản X-UI: {{ .Version }}\r\n" "xrayVersion" = "📡 Phiên bản Xray: {{ .XrayVersion }}\r\n" "ipv6" = "🌐 IPv6: {{ .IPv6 }}\r\n" "ipv4" = "🌐 IPv4: {{ .IPv4 }}\r\n" "ip" = "🌐 IP: {{ .IP }}\r\n" "ips" = "🔢 Các IP:\r\n{{ .IPs }}\r\n" "serverUpTime" = "⏳ Thời gian hoạt động của máy chủ: {{ .UpTime }} {{ .Unit }}\r\n" "serverLoad" = "📈 Tải máy chủ: {{ .Load1 }}, {{ .Load2 }}, {{ .Load3 }}\r\n" "serverMemory" = "📋 Bộ nhớ máy chủ: {{ .Current }}/{{ .Total }}\r\n" "tcpCount" = "🔹 Số lượng kết nối TCP: {{ .Count }}\r\n" "udpCount" = "🔸 Số lượng kết nối UDP: {{ .Count }}\r\n" "traffic" = "🚦 Lưu lượng: {{ .Total }} (↑{{ .Upload }},↓{{ .Download }})\r\n" "xrayStatus" = "ℹ️ Trạng thái Xray: {{ .State }}\r\n" "username" = "👤 Tên người dùng: {{ .Username }}\r\n" "password" = "👤 Mật khẩu: {{ .Password }}\r\n" "time" = "⏰ Thời gian: {{ .Time }}\r\n" "inbound" = "📍 Inbound: {{ .Remark }}\r\n" "port" = "🔌 Cổng: {{ .Port }}\r\n" "expire" = "📅 Ngày hết hạn: {{ .Time }}\r\n" "expireIn" = "📅 Hết hạn sau: {{ .Time }}\r\n" "active" = "💡 Đang hoạt động: {{ .Enable }}\r\n" "enabled" = "🚨 Đã bật: {{ .Enable }}\r\n" "online" = "🌐 Trạng thái kết nối: {{ .Status }}\r\n" "lastOnline" = "🔙 Lần online gần nhất: {{ .Time }}\r\n" "email" = "📧 Email: {{ .Email }}\r\n" "upload" = "🔼 Tải lên: ↑{{ .Upload }}\r\n" "download" = "🔽 Tải xuống: ↓{{ .Download }}\r\n" "total" = "📊 Tổng cộng: ↑↓{{ .UpDown }} / {{ .Total }}\r\n" "TGUser" = "👤 Người dùng Telegram: {{ .TelegramID }}\r\n" "exhaustedMsg" = "🚨 Sự cạn kiệt {{ .Type }}:\r\n" "exhaustedCount" = "🚨 Số lần cạn kiệt {{ .Type }}:\r\n" "onlinesCount" = "🌐 Khách hàng trực tuyến: {{ .Count }}\r\n" "disabled" = "🛑 Vô hiệu hóa: {{ .Disabled }}\r\n" "depleteSoon" = "🔜 Sắp cạn kiệt: {{ .Deplete }}\r\n\r\n" "backupTime" = "🗄 Thời gian sao lưu: {{ .Time }}\r\n" "refreshedOn" = "\r\n📋🔄 Đã cập nhật lần cuối vào: {{ .Time }}\r\n\r\n" "yes" = "✅ Có" "no" = "❌ Không" "received_id" = "🔑📥 ID đã được cập nhật." "received_password" = "🔑📥 Mật khẩu đã được cập nhật." "received_email" = "📧📥 Email đã được cập nhật." "received_comment" = "💬📥 Bình luận đã được cập nhật." "id_prompt" = "🔑 ID mặc định: {{ .ClientId }}\n\nVui lòng nhập ID của bạn." "pass_prompt" = "🔑 Mật khẩu mặc định: {{ .ClientPassword }}\n\nVui lòng nhập mật khẩu của bạn." "email_prompt" = "📧 Email mặc định: {{ .ClientEmail }}\n\nVui lòng nhập email của bạn." "comment_prompt" = "💬 Bình luận mặc định: {{ .ClientComment }}\n\nVui lòng nhập bình luận của bạn." "inbound_client_data_id" = "🔄 Kết nối vào: {{ .InboundRemark }}\n\n🔑 ID: {{ .ClientId }}\n📧 Email: {{ .ClientEmail }}\n📊 Dung lượng: {{ .ClientTraffic }}\n📅 Ngày hết hạn: {{ .ClientExp }}\n🌐 Giới hạn IP: {{ .IpLimit }}\n💬 Ghi chú: {{ .ClientComment }}\n\nBây giờ bạn có thể thêm khách hàng vào inbound!" "inbound_client_data_pass" = "🔄 Kết nối vào: {{ .InboundRemark }}\n\n🔑 Mật khẩu: {{ .ClientPass }}\n📧 Email: {{ .ClientEmail }}\n📊 Dung lượng: {{ .ClientTraffic }}\n📅 Ngày hết hạn: {{ .ClientExp }}\n🌐 Giới hạn IP: {{ .IpLimit }}\n💬 Ghi chú: {{ .ClientComment }}\n\nBây giờ bạn có thể thêm khách hàng vào inbound!" "cancel" = "❌ Quá trình đã bị hủy! \n\nBạn có thể bắt đầu lại bất cứ lúc nào bằng cách nhập /start. 🔄" "error_add_client" = "⚠️ Lỗi:\n\n {{ .error }}" "using_default_value" = "Được rồi, tôi sẽ sử dụng giá trị mặc định. 😊" "incorrect_input" = "Dữ liệu bạn nhập không hợp lệ.\nCác chuỗi phải liền mạch và không có dấu cách.\nVí dụ đúng: aaaaaa\nVí dụ sai: aaa aaa 🚫" "AreYouSure" = "Bạn có chắc không? 🤔" "SuccessResetTraffic" = "📧 Email: {{ .ClientEmail }}\n🏁 Kết quả: ✅ Thành công" "FailedResetTraffic" = "📧 Email: {{ .ClientEmail }}\n🏁 Kết quả: ❌ Thất bại \n\n🛠️ Lỗi: [ {{ .ErrorMessage }} ]" "FinishProcess" = "🔚 Quá trình đặt lại lưu lượng đã hoàn tất cho tất cả khách hàng." [tgbot.buttons] "closeKeyboard" = "❌ Đóng Bàn Phím" "cancel" = "❌ Hủy" "cancelReset" = "❌ Hủy Đặt Lại" "cancelIpLimit" = "❌ Hủy Giới Hạn IP" "confirmResetTraffic" = "✅ Xác Nhận Đặt Lại Lưu Lượng?" "confirmClearIps" = "✅ Xác Nhận Xóa Các IP?" "confirmRemoveTGUser" = "✅ Xác Nhận Xóa Người Dùng Telegram?" "confirmToggle" = "✅ Xác nhận Bật/Tắt người dùng?" "dbBackup" = "Tải bản sao lưu cơ sở dữ liệu" "serverUsage" = "Sử Dụng Máy Chủ" "getInbounds" = "Lấy cổng vào" "depleteSoon" = "Depleted Soon" "clientUsage" = "Lấy Sử Dụng" "onlines" = "Khách hàng trực tuyến" "commands" = "Lệnh" "refresh" = "🔄 Cập Nhật" "clearIPs" = "❌ Xóa IP" "removeTGUser" = "❌ Xóa Người Dùng Telegram" "selectTGUser" = "👤 Chọn Người Dùng Telegram" "selectOneTGUser" = "👤 Chọn một người dùng telegram:" "resetTraffic" = "📈 Đặt Lại Lưu Lượng" "resetExpire" = "📅 Thay đổi ngày hết hạn" "ipLog" = "🔢 Nhật ký địa chỉ IP" "ipLimit" = "🔢 Giới Hạn địa chỉ IP" "setTGUser" = "👤 Đặt Người Dùng Telegram" "toggle" = "🔘 Bật / Tắt" "custom" = "🔢 Tùy chỉnh" "confirmNumber" = "✅ Xác nhận: {{ .Num }}" "confirmNumberAdd" = "✅ Xác nhận thêm: {{ .Num }}" "limitTraffic" = "🚧 Giới hạn lưu lượng" "getBanLogs" = "Cấm nhật ký" "allClients" = "Tất cả Khách hàng" "addClient" = "Thêm Khách Hàng" "submitDisable" = "Gửi Dưới Dạng Vô Hiệu ☑️" "submitEnable" = "Gửi Dưới Dạng Kích Hoạt ✅" "use_default" = "🏷️ Sử Dụng Mặc Định" "change_id" = "⚙️🔑 ID" "change_password" = "⚙️🔑 Mật Khẩu" "change_email" = "⚙️📧 Email" "change_comment" = "⚙️💬 Bình Luận" "ResetAllTraffics" = "Đặt lại tất cả lưu lượng" "SortedTrafficUsageReport" = "Báo cáo sử dụng lưu lượng đã sắp xếp" [tgbot.answers] "successfulOperation" = "✅ Thành công!" "errorOperation" = "❗ Lỗi Trong Quá Trình Thực Hiện." "getInboundsFailed" = "❌ Không Thể Lấy Được Inbounds" "getClientsFailed" = "❌ Không thể lấy khách hàng." "canceled" = "❌ {{ .Email }} : Thao Tác Đã Bị Hủy." "clientRefreshSuccess" = "✅ {{ .Email }} : Cập Nhật Thành Công Cho Khách Hàng." "IpRefreshSuccess" = "✅ {{ .Email }} : Cập Nhật Thành Công Cho IPs." "TGIdRefreshSuccess" = "✅ {{ .Email }} : Cập Nhật Thành Công Cho Người Dùng Telegram." "resetTrafficSuccess" = "✅ {{ .Email }} : Đặt Lại Lưu Lượng Thành Công." "setTrafficLimitSuccess" = "✅ {{ .Email }} : Đã lưu thành công giới hạn lưu lượng." "expireResetSuccess" = "✅ {{ .Email }} : Đặt Lại Ngày Hết Hạn Thành Công." "resetIpSuccess" = "✅ {{ .Email }} : Giới Hạn IP {{ .Count }} Đã Được Lưu Thành Công." "clearIpSuccess" = "✅ {{ .Email }} : IP Đã Được Xóa Thành Công." "getIpLog" = "✅ {{ .Email }} : Lấy nhật ký IP Thành Công." "getUserInfo" = "✅ {{ .Email }} : Lấy Thông Tin Người Dùng Telegram Thành Công." "removedTGUserSuccess" = "✅ {{ .Email }} : Người Dùng Telegram Đã Được Xóa Thành Công." "enableSuccess" = "✅ {{ .Email }} : Đã Bật Thành Công." "disableSuccess" = "✅ {{ .Email }} : Đã Tắt Thành Công." "askToAddUserId" = "Cấu hình của bạn không được tìm thấy!\r\nVui lòng yêu cầu Quản trị viên sử dụng ID người dùng telegram của bạn trong cấu hình của bạn.\r\n\r\nID người dùng của bạn: {{ .TgUserID }}" "chooseClient" = "Chọn một Khách hàng cho Inbound {{ .Inbound }}" "chooseInbound" = "Chọn một Inbound" ================================================ FILE: web/translation/translate.zh_CN.toml ================================================ "username" = "用户名" "password" = "密码" "login" = "登录" "confirm" = "确定" "cancel" = "取消" "close" = "关闭" "create" = "创建" "update" = "更新" "copy" = "复制" "copied" = "已复制" "download" = "下载" "remark" = "备注" "enable" = "启用" "protocol" = "协议" "search" = "搜索" "filter" = "筛选" "loading" = "加载中..." "second" = "秒" "minute" = "分钟" "hour" = "小时" "day" = "天" "check" = "查看" "indefinite" = "无限期" "unlimited" = "无限制" "none" = "无" "qrCode" = "二维码" "info" = "更多信息" "edit" = "编辑" "delete" = "删除" "reset" = "重置" "noData" = "无数据。" "copySuccess" = "复制成功" "sure" = "确定" "encryption" = "加密" "useIPv4ForHost" = "使用 IPv4 连接主机" "transmission" = "传输" "host" = "主机" "path" = "路径" "camouflage" = "伪装" "status" = "状态" "enabled" = "开启" "disabled" = "关闭" "depleted" = "耗尽" "depletingSoon" = "即将耗尽" "offline" = "离线" "online" = "在线" "domainName" = "域名" "monitor" = "监听" "certificate" = "数字证书" "fail" = "失败" "comment" = "评论" "success" = "成功" "lastOnline" = "上次在线" "getVersion" = "获取版本" "install" = "安装" "clients" = "客户端" "usage" = "使用情况" "twoFactorCode" = "代码" "remained" = "剩余" "security" = "安全" "secAlertTitle" = "安全警报" "secAlertSsl" = "此连接不安全。在激活 TLS 进行数据保护之前,请勿输入敏感信息。" "secAlertConf" = "某些设置易受攻击。建议加强安全协议以防止潜在漏洞。" "secAlertSSL" = "面板缺少安全连接。请安装 TLS 证书以保护数据安全。" "secAlertPanelPort" = "面板默认端口存在安全风险。请配置随机端口或特定端口。" "secAlertPanelURI" = "面板默认 URI 路径不安全。请配置复杂的 URI 路径。" "secAlertSubURI" = "订阅默认 URI 路径不安全。请配置复杂的 URI 路径。" "secAlertSubJsonURI" = "订阅 JSON 默认 URI 路径不安全。请配置复杂的 URI 路径。" "emptyDnsDesc" = "未添加DNS服务器。" "emptyFakeDnsDesc" = "未添加Fake DNS服务器。" "emptyBalancersDesc" = "未添加负载均衡器。" "emptyReverseDesc" = "未添加反向代理。" "somethingWentWrong" = "出了点问题" [subscription] "title" = "订阅信息" "subId" = "订阅 ID" "status" = "状态" "downloaded" = "已下载" "uploaded" = "已上传" "expiry" = "到期" "totalQuota" = "总配额" "individualLinks" = "单独链接" "active" = "启用" "inactive" = "停用" "unlimited" = "无限制" "noExpiry" = "无到期" [menu] "theme" = "主题" "dark" = "暗色" "ultraDark" = "超暗色" "dashboard" = "系统状态" "inbounds" = "入站列表" "settings" = "面板设置" "xray" = "Xray 设置" "logout" = "退出登录" "link" = "管理" [pages.login] "hello" = "你好" "title" = "欢迎" "loginAgain" = "登录时效已过,请重新登录" [pages.login.toasts] "invalidFormData" = "数据格式错误" "emptyUsername" = "请输入用户名" "emptyPassword" = "请输入密码" "wrongUsernameOrPassword" = "用户名、密码或双重验证码无效。" "successLogin" = "您已成功登录您的账户。" [pages.index] "title" = "系统状态" "cpu" = "CPU" "logicalProcessors" = "逻辑处理器" "frequency" = "频率" "swap" = "交换分区" "storage" = "存储" "memory" = "内存" "threads" = "线程" "xrayStatus" = "Xray" "stopXray" = "停止" "restartXray" = "重启" "xraySwitch" = "版本" "xraySwitchClick" = "选择你要切换到的版本" "xraySwitchClickDesk" = "请谨慎选择,因为较旧版本可能与当前配置不兼容" "xrayStatusUnknown" = "未知" "xrayStatusRunning" = "运行中" "xrayStatusStop" = "停止" "xrayStatusError" = "错误" "xrayErrorPopoverTitle" = "运行Xray时发生错误" "operationHours" = "系统正常运行时间" "systemLoad" = "系统负载" "systemLoadDesc" = "过去 1、5 和 15 分钟的系统平均负载" "connectionCount" = "连接数" "ipAddresses" = "IP地址" "toggleIpVisibility" = "切换IP可见性" "overallSpeed" = "整体速度" "upload" = "上传" "download" = "下载" "totalData" = "总数据" "sent" = "已发送" "received" = "已接收" "documentation" = "文档" "xraySwitchVersionDialog" = "您确定要更改Xray版本吗?" "xraySwitchVersionDialogDesc" = "这将把Xray版本更改为#version#。" "xraySwitchVersionPopover" = "Xray 更新成功" "geofileUpdateDialog" = "您确定要更新地理文件吗?" "geofileUpdateDialogDesc" = "这将更新 #filename# 文件。" "geofilesUpdateDialogDesc" = "这将更新所有文件。" "geofilesUpdateAll" = "全部更新" "geofileUpdatePopover" = "地理文件更新成功" "dontRefresh" = "安装中,请勿刷新此页面" "logs" = "日志" "config" = "配置" "backup" = "备份" "backupTitle" = "备份和恢复数据库" "exportDatabase" = "备份" "exportDatabaseDesc" = "点击下载包含当前数据库备份的 .db 文件到您的设备。" "importDatabase" = "恢复" "importDatabaseDesc" = "点击选择并上传设备中的 .db 文件以从备份恢复数据库。" "importDatabaseSuccess" = "数据库导入成功" "importDatabaseError" = "导入数据库时出错" "readDatabaseError" = "读取数据库时出错" "getDatabaseError" = "检索数据库时出错" "getConfigError" = "检索配置文件时出错" [pages.inbounds] "allTimeTraffic" = "累计总流量" "allTimeTrafficUsage" = "所有时间总使用量" "title" = "入站列表" "totalDownUp" = "总上传 / 下载" "totalUsage" = "总用量" "inboundCount" = "入站数量" "operate" = "菜单" "enable" = "启用" "remark" = "备注" "protocol" = "协议" "port" = "端口" "portMap" = "端口映射" "traffic" = "流量" "details" = "详细信息" "transportConfig" = "传输配置" "expireDate" = "到期时间" "createdAt" = "创建时间" "updatedAt" = "更新时间" "resetTraffic" = "重置流量" "addInbound" = "添加入站" "generalActions" = "通用操作" "autoRefresh" = "自动刷新" "autoRefreshInterval" = "间隔" "modifyInbound" = "修改入站" "deleteInbound" = "删除入站" "deleteInboundContent" = "确定要删除入站吗?" "deleteClient" = "删除客户端" "deleteClientContent" = "确定要删除客户端吗?" "resetTrafficContent" = "确定要重置流量吗?" "copyLink" = "复制链接" "address" = "地址" "network" = "网络" "destinationPort" = "目标端口" "targetAddress" = "目标地址" "monitorDesc" = "留空表示监听所有 IP" "meansNoLimit" = "= 无限制(单位:GB)" "totalFlow" = "总流量" "leaveBlankToNeverExpire" = "留空表示永不过期" "noRecommendKeepDefault" = "建议保留默认值" "certificatePath" = "文件路径" "certificateContent" = "文件内容" "publicKey" = "公钥" "privatekey" = "私钥" "clickOnQRcode" = "点击二维码复制" "client" = "客户" "export" = "导出链接" "clone" = "克隆" "cloneInbound" = "克隆" "cloneInboundContent" = "此入站规则除端口(Port)、监听 IP(Listening IP)和客户端(Clients)以外的所有配置都将应用于克隆" "cloneInboundOk" = "创建克隆" "resetAllTraffic" = "重置所有入站流量" "resetAllTrafficTitle" = "重置所有入站流量" "resetAllTrafficContent" = "确定要重置所有入站流量吗?" "resetInboundClientTraffics" = "重置客户端流量" "resetInboundClientTrafficTitle" = "重置所有客户端流量" "resetInboundClientTrafficContent" = "确定要重置此入站客户端的所有流量吗?" "resetAllClientTraffics" = "重置所有客户端流量" "resetAllClientTrafficTitle" = "重置所有客户端流量" "resetAllClientTrafficContent" = "确定要重置所有客户端的所有流量吗?" "delDepletedClients" = "删除流量耗尽的客户端" "delDepletedClientsTitle" = "删除流量耗尽的客户端" "delDepletedClientsContent" = "确定要删除所有流量耗尽的客户端吗?" "email" = "电子邮件" "emailDesc" = "电子邮件必须完全唯一" "IPLimit" = "IP 限制" "IPLimitDesc" = "如果数量超过设置值,则禁用入站流量。(0 = 禁用)" "IPLimitlog" = "IP 日志" "IPLimitlogDesc" = "IP 历史日志(要启用被禁用的入站流量,请清除日志)" "IPLimitlogclear" = "清除日志" "setDefaultCert" = "从面板设置证书" "telegramDesc" = "请提供Telegram聊天ID。(在机器人中使用'/id'命令)或(@userinfobot" "subscriptionDesc" = "要找到你的订阅 URL,请导航到“详细信息”。此外,你可以为多个客户端使用相同的名称。" "info" = "信息" "same" = "相同" "inboundData" = "入站数据" "exportInbound" = "导出入站规则" "import" = "导入" "importInbound" = "导入入站规则" "periodicTrafficResetTitle" = "流量重置" "periodicTrafficResetDesc" = "按指定间隔自动重置流量计数器" "lastReset" = "上次重置" [pages.client] "add" = "添加客户端" "edit" = "编辑客户端" "submitAdd" = "添加客户端" "submitEdit" = "保存修改" "clientCount" = "客户端数量" "bulk" = "批量创建" "method" = "方法" "first" = "置顶" "last" = "置底" "prefix" = "前缀" "postfix" = "后缀" "delayedStart" = "首次使用后开始" "expireDays" = "期间" "days" = "天" "renew" = "自动续订" "renewDesc" = "到期后自动续订。(0 = 禁用)(单位: 天)" [pages.inbounds.periodicTrafficReset] "never" = "从不" "daily" = "每日" "weekly" = "每周" "monthly" = "每月" [pages.inbounds.toasts] "obtain" = "获取" "updateSuccess" = "更新成功" "logCleanSuccess" = "日志已清除" "inboundsUpdateSuccess" = "入站连接已成功更新" "inboundUpdateSuccess" = "入站连接已成功更新" "inboundCreateSuccess" = "入站连接已成功创建" "inboundDeleteSuccess" = "入站连接已成功删除" "inboundClientAddSuccess" = "已添加入站客户端" "inboundClientDeleteSuccess" = "入站客户端已删除" "inboundClientUpdateSuccess" = "入站客户端已更新" "delDepletedClientsSuccess" = "所有耗尽客户端已删除" "resetAllClientTrafficSuccess" = "客户端所有流量已重置" "resetAllTrafficSuccess" = "所有流量已重置" "resetInboundClientTrafficSuccess" = "流量已重置" "trafficGetError" = "获取流量数据时出错" "getNewX25519CertError" = "获取X25519证书时出错。" "getNewmldsa65Error" = "获取mldsa65证书时出错。" "getNewVlessEncError" = "获取VlessEnc证书时出错。" [pages.inbounds.stream.general] "request" = "请求" "response" = "响应" "name" = "名称" "value" = "值" [pages.inbounds.stream.tcp] "version" = "版本" "method" = "方法" "path" = "路径" "status" = "状态" "statusDescription" = "状态说明" "requestHeader" = "请求头" "responseHeader" = "响应头" [pages.settings] "title" = "面板设置" "save" = "保存" "infoDesc" = "此处的所有更改都需要保存并重启面板才能生效" "restartPanel" = "重启面板" "restartPanelDesc" = "确定要重启面板吗?若重启后无法访问面板,请前往服务器查看面板日志信息" "restartPanelSuccess" = "面板已成功重启" "actions" = "操作" "resetDefaultConfig" = "重置为默认配置" "panelSettings" = "常规" "securitySettings" = "安全设定" "TGBotSettings" = "Telegram 机器人配置" "panelListeningIP" = "面板监听 IP" "panelListeningIPDesc" = "默认留空监听所有 IP" "panelListeningDomain" = "面板监听域名" "panelListeningDomainDesc" = "默认情况下留空以监视所有域名和 IP 地址" "panelPort" = "面板监听端口" "panelPortDesc" = "重启面板生效" "publicKeyPath" = "面板证书公钥文件路径" "publicKeyPathDesc" = "填写一个 '/' 开头的绝对路径" "privateKeyPath" = "面板证书密钥文件路径" "privateKeyPathDesc" = "填写一个 '/' 开头的绝对路径" "panelUrlPath" = "面板 url 根路径" "panelUrlPathDesc" = "必须以 '/' 开头,以 '/' 结尾" "pageSize" = "分页大小" "pageSizeDesc" = "定义入站表的页面大小。设置 0 表示禁用" "remarkModel" = "备注模型和分隔符" "datepicker" = "日期选择器" "datepickerPlaceholder" = "选择日期" "datepickerDescription" = "选择器日历类型指定到期日期" "sampleRemark" = "备注示例" "oldUsername" = "原用户名" "currentPassword" = "原密码" "newUsername" = "新用户名" "newPassword" = "新密码" "telegramBotEnable" = "启用 Telegram 机器人" "telegramBotEnableDesc" = "启用 Telegram 机器人功能" "telegramToken" = "Telegram 机器人令牌(token)" "telegramTokenDesc" = "从 '@BotFather' 获取的 Telegram 机器人令牌" "telegramProxy" = "SOCKS5 Proxy" "telegramProxyDesc" = "启用 SOCKS5 代理连接到 Telegram(根据指南调整设置)" "telegramAPIServer" = "Telegram API Server" "telegramAPIServerDesc" = "要使用的 Telegram API 服务器。留空以使用默认服务器。" "telegramChatId" = "管理员聊天 ID" "telegramChatIdDesc" = "Telegram 管理员聊天 ID (多个以逗号分隔)(可通过 @userinfobot 获取,或在机器人中使用 '/id' 命令获取)" "telegramNotifyTime" = "通知时间" "telegramNotifyTimeDesc" = "设置周期性的 Telegram 机器人通知时间(使用 crontab 时间格式)" "tgNotifyBackup" = "数据库备份" "tgNotifyBackupDesc" = "发送带有报告的数据库备份文件" "tgNotifyLogin" = "登录通知" "tgNotifyLoginDesc" = "当有人试图登录你的面板时显示用户名、IP 地址和时间" "sessionMaxAge" = "会话时长" "sessionMaxAgeDesc" = "保持登录状态的时长(单位:分钟)" "expireTimeDiff" = "到期通知阈值" "expireTimeDiffDesc" = "达到此阈值时,将收到有关到期时间的通知(单位:天)" "trafficDiff" = "流量耗尽阈值" "trafficDiffDesc" = "达到此阈值时,将收到有关流量耗尽的通知(单位:GB)" "tgNotifyCpu" = "CPU 负载通知阈值" "tgNotifyCpuDesc" = "CPU 负载超过此阈值时,将收到通知(单位:%)" "timeZone" = "时区" "timeZoneDesc" = "定时任务将按照该时区的时间运行" "subSettings" = "订阅设置" "subEnable" = "启用订阅服务" "subEnableDesc" = "启用订阅服务功能" "subJsonEnable" = "单独启用/禁用 JSON 订阅端点。" "subTitle" = "订阅标题" "subTitleDesc" = "在VPN客户端中显示的标题" "subSupportUrl" = "支持链接" "subSupportUrlDesc" = "VPN 客户端中显示的技术支持链接" "subProfileUrl" = "个人资料链接" "subProfileUrlDesc" = "VPN 客户端中显示的网站链接" "subAnnounce" = "公告" "subAnnounceDesc" = "VPN 客户端中显示的公告文本" "subEnableRouting" = "启用路由" "subEnableRoutingDesc" = "在 VPN 客户端中启用路由的全局设置。(僅限 Happ)" "subRoutingRules" = "路由規則" "subRoutingRulesDesc" = "VPN 用戶端的全域路由規則。(僅限 Happ)" "subListen" = "监听 IP" "subListenDesc" = "订阅服务监听的 IP 地址(留空表示监听所有 IP)" "subPort" = "监听端口" "subPortDesc" = "订阅服务监听的端口号(必须是未使用的端口)" "subCertPath" = "公钥路径" "subCertPathDesc" = "订阅服务使用的公钥文件路径(以 '/' 开头)" "subKeyPath" = "私钥路径" "subKeyPathDesc" = "订阅服务使用的私钥文件路径(以 '/' 开头)" "subPath" = "URI 路径" "subPathDesc" = "订阅服务使用的 URI 路径(以 '/' 开头,以 '/' 结尾)" "subDomain" = "监听域名" "subDomainDesc" = "订阅服务监听的域名(留空表示监听所有域名和 IP)" "subUpdates" = "更新间隔" "subUpdatesDesc" = "客户端应用中订阅 URL 的更新间隔(单位:小时)" "subEncrypt" = "编码" "subEncryptDesc" = "订阅服务返回的内容将采用 Base64 编码" "subShowInfo" = "显示使用信息" "subShowInfoDesc" = "客户端应用中将显示剩余流量和日期信息" "subURI" = "反向代理 URI" "subURIDesc" = "用于代理后面的订阅 URL 的 URI 路径" "externalTrafficInformEnable" = "外部交通通知" "externalTrafficInformEnableDesc" = "每次流量更新时通知外部 API" "externalTrafficInformURI" = "外部流量通知 URI" "externalTrafficInformURIDesc" = "流量更新将发送到此 URI" "fragment" = "分片" "fragmentDesc" = "启用 TLS hello 数据包分片" "fragmentSett" = "设置" "noisesDesc" = "启用 Noises." "noisesSett" = "Noises 设置" "mux" = "多路复用器" "muxDesc" = "在已建立的数据流内传输多个独立的数据流" "muxSett" = "复用器设置" "direct" = "直接连接" "directDesc" = "直接与特定国家的域或IP范围建立连接" "notifications" = "通知" "certs" = "证书" "externalTraffic" = "外部流量" "dateAndTime" = "日期和时间" "proxyAndServer" = "代理和服务器" "intervals" = "间隔" "information" = "信息" "language" = "语言" "telegramBotLanguage" = "Telegram 机器人语言" [pages.xray] "title" = "Xray 配置" "save" = "保存" "restart" = "重新启动 Xray" "restartSuccess" = "Xray 已成功重新启动" "stopSuccess" = "Xray 已成功停止" "restartError" = "重启Xray时发生错误。" "stopError" = "停止Xray时发生错误。" "basicTemplate" = "基础配置" "advancedTemplate" = "高级配置" "generalConfigs" = "常规配置" "generalConfigsDesc" = "这些选项将决定常规配置" "logConfigs" = "日志" "logConfigsDesc" = "日志可能会影响服务器的性能,建议仅在需要时启用" "blockConfigsDesc" = "这些选项将阻止用户连接到特定协议和网站" "basicRouting" = "基本路由" "blockConnectionsConfigsDesc" = "这些选项将根据特定的请求国家阻止流量。" "directConnectionsConfigsDesc" = "直接连接确保特定的流量不会通过其他服务器路由。" "blockips" = "阻止IP" "blockdomains" = "阻止域名" "directips" = "直接IP" "directdomains" = "直接域名" "ipv4Routing" = "IPv4 路由" "ipv4RoutingDesc" = "此选项将仅通过 IPv4 路由到目标域" "warpRouting" = "WARP 路由" "warpRoutingDesc" = "注意:在使用这些选项之前,请按照面板 GitHub 上的步骤在你的服务器上以 socks5 代理模式安装 WARP。WARP 将通过 Cloudflare 服务器将流量路由到网站。" "Template" = "高级 Xray 配置模板" "TemplateDesc" = "最终的 Xray 配置文件将基于此模板生成" "FreedomStrategy" = "Freedom 协议策略" "FreedomStrategyDesc" = "设置 Freedom 协议中网络的输出策略" "RoutingStrategy" = "配置路由域策略" "RoutingStrategyDesc" = "设置 DNS 解析的整体路由策略" "outboundTestUrl" = "出站测试 URL" "outboundTestUrlDesc" = "测试出站连接时使用的 URL" "Torrent" = "屏蔽 BitTorrent 协议" "Inbounds" = "入站规则" "InboundsDesc" = "接受来自特定客户端的流量" "Outbounds" = "出站规则" "Balancers" = "负载均衡" "OutboundsDesc" = "设置出站流量传出方式" "Routings" = "路由规则" "RoutingsDesc" = "每条规则的优先级都很重要" "completeTemplate" = "全部" "logLevel" = "日志级别" "logLevelDesc" = "错误日志的日志级别,用于指示需要记录的信息" "accessLog" = "访问日志" "accessLogDesc" = "访问日志的文件路径。特殊值 'none' 禁用访问日志" "errorLog" = "错误日志" "errorLogDesc" = "错误日志的文件路径。特殊值 'none' 禁用错误日志" "dnsLog" = "DNS 日志" "dnsLogDesc" = "是否启用 DNS 查询日志" "maskAddress" = "隐藏地址" "maskAddressDesc" = "IP 地址掩码,启用时会自动替换日志中出现的 IP 地址。" "statistics" = "统计" "statsInboundUplink" = "入站上传统计" "statsInboundUplinkDesc" = "启用所有入站代理的上行流量统计收集。" "statsInboundDownlink" = "入站下载统计" "statsInboundDownlinkDesc" = "启用所有入站代理的下行流量统计收集。" "statsOutboundUplink" = "出站上传统计" "statsOutboundUplinkDesc" = "启用所有出站代理的上行流量统计收集。" "statsOutboundDownlink" = "出站下载统计" "statsOutboundDownlinkDesc" = "启用所有出站代理的下行流量统计收集。" [pages.xray.rules] "first" = "置顶" "last" = "置底" "up" = "向上" "down" = "向下" "source" = "来源" "dest" = "目的地址" "inbound" = "入站" "outbound" = "出站" "balancer" = "负载均衡" "info" = "信息" "add" = "添加规则" "edit" = "编辑规则" "useComma" = "逗号分隔的项目" [pages.xray.outbound] "addOutbound" = "添加出站" "addReverse" = "添加反向" "editOutbound" = "编辑出站" "editReverse" = "编辑反向" "tag" = "标签" "tagDesc" = "唯一标签" "address" = "地址" "reverse" = "反向" "domain" = "域名" "type" = "类型" "bridge" = "Bridge" "portal" = "Portal" "link" = "链接" "intercon" = "互连" "settings" = "设置" "accountInfo" = "帐户信息" "outboundStatus" = "出站状态" "sendThrough" = "发送通过" "test" = "测试" "testResult" = "测试结果" "testing" = "正在测试连接..." "testSuccess" = "测试成功" "testFailed" = "测试失败" "testError" = "测试出站失败" [pages.xray.balancer] "addBalancer" = "添加负载均衡" "editBalancer" = "编辑负载均衡" "balancerStrategy" = "策略" "balancerSelectors" = "选择器" "tag" = "标签" "tagDesc" = "唯一标签" "balancerDesc" = "无法同时使用 balancerTag 和 outboundTag。如果同时使用,则只有 outboundTag 会生效。" [pages.xray.wireguard] "secretKey" = "密钥" "publicKey" = "公钥" "allowedIPs" = "允许的 IP" "endpoint" = "端点" "psk" = "共享密钥" "domainStrategy" = "域策略" [pages.xray.tun] "nameDesc" = "TUN 接口的名称。默认值为 'xray0'" "mtuDesc" = "最大传输单元。数据包的最大大小。默认值为 1500" "userLevel" = "用户级别" "userLevelDesc" = "通过此入站的所有连接都将使用此用户级别。默认值为 0" [pages.xray.dns] "enable" = "启用 DNS" "enableDesc" = "启用内置 DNS 服务器" "tag" = "DNS 入站标签" "tagDesc" = "此标签将在路由规则中可用作入站标签" "clientIp" = "客户端IP" "clientIpDesc" = "用于在DNS查询期间通知服务器指定的IP位置" "disableCache" = "禁用缓存" "disableCacheDesc" = "禁用DNS缓存" "disableFallback" = "禁用回退" "disableFallbackDesc" = "禁用回退DNS查询" "disableFallbackIfMatch" = "匹配时禁用回退" "disableFallbackIfMatchDesc" = "当DNS服务器的匹配域名列表命中时,禁用回退DNS查询" "enableParallelQuery" = "启用并行查询" "enableParallelQueryDesc" = "启用并行DNS查询到多个服务器以实现更快的解析" "strategy" = "查询策略" "strategyDesc" = "解析域名的总体策略" "add" = "添加服务器" "edit" = "编辑服务器" "domains" = "域" "expectIPs" = "预期 IP" "unexpectIPs" = "意外IP" "useSystemHosts" = "使用系统Hosts" "useSystemHostsDesc" = "使用已安装系统的hosts文件" "usePreset" = "使用模板" "dnsPresetTitle" = "DNS模板" "dnsPresetFamily" = "家庭" [pages.xray.fakedns] "add" = "添加假 DNS" "edit" = "编辑假 DNS" "ipPool" = "IP 池子网" "poolSize" = "池大小" [pages.settings.security] "admin" = "管理员凭据" "twoFactor" = "双重验证" "twoFactorEnable" = "启用2FA" "twoFactorEnableDesc" = "增加额外的验证层以提高安全性。" "twoFactorModalSetTitle" = "启用双重认证" "twoFactorModalDeleteTitle" = "停用双重认证" "twoFactorModalSteps" = "要设定双重认证,请执行以下步骤:" "twoFactorModalFirstStep" = "1. 在认证应用程序中扫描此QR码,或复制QR码附近的令牌并粘贴到应用程序中" "twoFactorModalSecondStep" = "2. 输入应用程序中的验证码" "twoFactorModalRemoveStep" = "输入应用程序中的验证码以移除双重认证。" "twoFactorModalChangeCredentialsTitle" = "更改凭据" "twoFactorModalChangeCredentialsStep" = "输入应用程序中的代码以更改管理员凭据。" "twoFactorModalSetSuccess" = "双因素认证已成功建立" "twoFactorModalDeleteSuccess" = "双因素认证已成功删除" "twoFactorModalError" = "验证码错误" [pages.settings.toasts] "modifySettings" = "参数已更改。" "getSettings" = "获取参数时发生错误" "modifyUserError" = "更改管理员凭据时发生错误。" "modifyUser" = "您已成功更改管理员凭据。" "originalUserPassIncorrect" = "原用户名或原密码错误" "userPassMustBeNotEmpty" = "新用户名和新密码不能为空" "getOutboundTrafficError" = "获取出站流量错误" "resetOutboundTrafficError" = "重置出站流量错误" [tgbot] "keyboardClosed" = "❌ 自定义键盘已关闭!" "noResult" = "❗ 没有结果!" "noQuery" = "❌ 未找到查询!请再次使用该命令!" "wentWrong" = "❌ 出了点问题!" "noIpRecord" = "❗ 没有IP记录!" "noInbounds" = "❗ 未找到入站!" "unlimited" = "♾ 无限(重置)" "add" = "添加" "month" = "月" "months" = "月" "day" = "天" "days" = "天" "hours" = "小时" "minutes" = "分钟" "unknown" = "未知" "inbounds" = "入站" "clients" = "客户端" "offline" = "🔴 离线" "online" = "🟢 在线" [tgbot.commands] "unknown" = "❗ 未知命令" "pleaseChoose" = "👇 请选择:\r\n" "help" = "🤖 欢迎使用本机器人!它旨在为您提供来自服务器的特定数据,并允许您进行必要的修改。\r\n\r\n" "start" = "👋 你好,{{ .Firstname }}。\r\n" "welcome" = "🤖 欢迎来到 {{ .Hostname }} 管理机器人。\r\n" "status" = "✅ 机器人正常运行!" "usage" = "❗ 请输入要搜索的文本!" "getID" = "🆔 您的 ID 为:{{ .ID }}" "helpAdminCommands" = "要重新启动 Xray Core:\r\n/restart\r\n\r\n要搜索客户电子邮件:\r\n/usage [电子邮件]\r\n\r\n要搜索入站(带有客户统计数据):\r\n/inbound [备注]\r\n\r\nTelegram聊天ID:\r\n/id" "helpClientCommands" = "要搜索统计数据,请使用以下命令:\r\n/usage [电子邮件]\r\n\r\nTelegram聊天ID:\r\n/id" "restartUsage" = "\r\n\r\n/restart" "restartSuccess" = "✅ 操作成功!" "restartFailed" = "❗ 操作错误。\r\n\r\n错误: {{ .Error }}." "xrayNotRunning" = "❗ Xray Core 未运行。" "startDesc" = "显示主菜单" "helpDesc" = "机器人帮助" "statusDesc" = "检查机器人状态" "idDesc" = "显示您的 Telegram ID" [tgbot.messages] "cpuThreshold" = "🔴 CPU 使用率为 {{ .Percent }}%,超过阈值 {{ .Threshold }}%" "selectUserFailed" = "❌ 用户选择错误!" "userSaved" = "✅ 电报用户已保存。" "loginSuccess" = "✅ 成功登录到面板。\r\n" "loginFailed" = "❗️ 面板登录失败。\r\n" "2faFailed" = "2FA 失败" "report" = "🕰 定时报告:{{ .RunTime }}\r\n" "datetime" = "⏰ 日期时间:{{ .DateTime }}\r\n" "hostname" = "💻 主机名:{{ .Hostname }}\r\n" "version" = "🚀 X-UI 版本:{{ .Version }}\r\n" "xrayVersion" = "📡 Xray 版本: {{ .XrayVersion }}\r\n" "ipv6" = "🌐 IPv6:{{ .IPv6 }}\r\n" "ipv4" = "🌐 IPv4:{{ .IPv4 }}\r\n" "ip" = "🌐 IP:{{ .IP }}\r\n" "ips" = "🔢 IP 地址:\r\n{{ .IPs }}\r\n" "serverUpTime" = "⏳ 服务器运行时间:{{ .UpTime }} {{ .Unit }}\r\n" "serverLoad" = "📈 服务器负载:{{ .Load1 }}, {{ .Load2 }}, {{ .Load3 }}\r\n" "serverMemory" = "📋 服务器内存:{{ .Current }}/{{ .Total }}\r\n" "tcpCount" = "🔹 TCP 连接数:{{ .Count }}\r\n" "udpCount" = "🔸 UDP 连接数:{{ .Count }}\r\n" "traffic" = "🚦 流量:{{ .Total }} (↑{{ .Upload }},↓{{ .Download }})\r\n" "xrayStatus" = "ℹ️ Xray 状态:{{ .State }}\r\n" "username" = "👤 用户名:{{ .Username }}\r\n" "password" = "👤 密码: {{ .Password }}\r\n" "time" = "⏰ 时间:{{ .Time }}\r\n" "inbound" = "📍 入站:{{ .Remark }}\r\n" "port" = "🔌 端口:{{ .Port }}\r\n" "expire" = "📅 过期日期:{{ .Time }}\r\n" "expireIn" = "📅 剩余时间:{{ .Time }}\r\n" "active" = "💡 激活:{{ .Enable }}\r\n" "enabled" = "🚨 已启用:{{ .Enable }}\r\n" "online" = "🌐 连接状态:{{ .Status }}\r\n" "lastOnline" = "🔙 上次在线: {{ .Time }}\r\n" "email" = "📧 邮箱:{{ .Email }}\r\n" "upload" = "🔼 上传↑:{{ .Upload }}\r\n" "download" = "🔽 下载↓:{{ .Download }}\r\n" "total" = "📊 总计:{{ .UpDown }} / {{ .Total }}\r\n" "TGUser" = "👤 电报用户:{{ .TelegramID }}\r\n" "exhaustedMsg" = "🚨 耗尽的 {{ .Type }}:\r\n" "exhaustedCount" = "🚨 耗尽的 {{ .Type }} 数量:\r\n" "onlinesCount" = "🌐 在线客户:{{ .Count }}\r\n" "disabled" = "🛑 禁用:{{ .Disabled }}\r\n" "depleteSoon" = "🔜 即将耗尽:{{ .Deplete }}\r\n\r\n" "backupTime" = "🗄 备份时间:{{ .Time }}\r\n" "refreshedOn" = "\r\n📋🔄 刷新时间:{{ .Time }}\r\n\r\n" "yes" = "✅ 是的" "no" = "❌ 没有" "received_id" = "🔑📥 ID 已更新。" "received_password" = "🔑📥 密码已更新。" "received_email" = "📧📥 邮箱已更新。" "received_comment" = "💬📥 评论已更新。" "id_prompt" = "🔑 默认 ID: {{ .ClientId }}\n\n请输入您的 ID。" "pass_prompt" = "🔑 默认密码: {{ .ClientPassword }}\n\n请输入您的密码。" "email_prompt" = "📧 默认邮箱: {{ .ClientEmail }}\n\n请输入您的邮箱。" "comment_prompt" = "💬 默认评论: {{ .ClientComment }}\n\n请输入您的评论。" "inbound_client_data_id" = "🔄 入站: {{ .InboundRemark }}\n\n🔑 ID: {{ .ClientId }}\n📧 邮箱: {{ .ClientEmail }}\n📊 流量: {{ .ClientTraffic }}\n📅 到期日期: {{ .ClientExp }}\n🌐 IP 限制: {{ .IpLimit }}\n💬 备注: {{ .ClientComment }}\n\n你现在可以将客户添加到入站了!" "inbound_client_data_pass" = "🔄 入站: {{ .InboundRemark }}\n\n🔑 密码: {{ .ClientPass }}\n📧 邮箱: {{ .ClientEmail }}\n📊 流量: {{ .ClientTraffic }}\n📅 到期日期: {{ .ClientExp }}\n🌐 IP 限制: {{ .IpLimit }}\n💬 备注: {{ .ClientComment }}\n\n你现在可以将客户添加到入站了!" "cancel" = "❌ 进程已取消!\n\n您可以随时使用 /start 重新开始。 🔄" "error_add_client" = "⚠️ 错误:\n\n {{ .error }}" "using_default_value" = "好的,我会使用默认值。 😊" "incorrect_input" = "您的输入无效。\n短语应连续输入,不能有空格。\n正确示例: aaaaaa\n错误示例: aaa aaa 🚫" "AreYouSure" = "你确定吗?🤔" "SuccessResetTraffic" = "📧 邮箱: {{ .ClientEmail }}\n🏁 结果: ✅ 成功" "FailedResetTraffic" = "📧 邮箱: {{ .ClientEmail }}\n🏁 结果: ❌ 失败 \n\n🛠️ 错误: [ {{ .ErrorMessage }} ]" "FinishProcess" = "🔚 所有客户的流量重置已完成。" [tgbot.buttons] "closeKeyboard" = "❌ 关闭键盘" "cancel" = "❌ 取消" "cancelReset" = "❌ 取消重置" "cancelIpLimit" = "❌ 取消 IP 限制" "confirmResetTraffic" = "✅ 确认重置流量?" "confirmClearIps" = "✅ 确认清除 IP?" "confirmRemoveTGUser" = "✅ 确认移除 Telegram 用户?" "confirmToggle" = "✅ 确认启用/禁用用户?" "dbBackup" = "获取数据库备份" "serverUsage" = "服务器使用情况" "getInbounds" = "获取入站信息" "depleteSoon" = "即将耗尽" "clientUsage" = "获取使用情况" "onlines" = "在线客户端" "commands" = "命令" "refresh" = "🔄 刷新" "clearIPs" = "❌ 清除 IP" "removeTGUser" = "❌ 移除 Telegram 用户" "selectTGUser" = "👤 选择 Telegram 用户" "selectOneTGUser" = "👤 选择一个 Telegram 用户:" "resetTraffic" = "📈 重置流量" "resetExpire" = "📅 更改到期日期" "ipLog" = "🔢 IP 日志" "ipLimit" = "🔢 IP 限制" "setTGUser" = "👤 设置 Telegram 用户" "toggle" = "🔘 启用/禁用" "custom" = "🔢 风俗" "confirmNumber" = "✅ 确认: {{ .Num }}" "confirmNumberAdd" = "✅ 确认添加:{{ .Num }}" "limitTraffic" = "🚧 流量限制" "getBanLogs" = "禁止日志" "allClients" = "所有客户" "addClient" = "添加客户" "submitDisable" = "提交为禁用 ☑️" "submitEnable" = "提交为启用 ✅" "use_default" = "🏷️ 使用默认" "change_id" = "⚙️🔑 ID" "change_password" = "⚙️🔑 密码" "change_email" = "⚙️📧 邮箱" "change_comment" = "⚙️💬 评论" "ResetAllTraffics" = "重置所有流量" "SortedTrafficUsageReport" = "排序的流量使用报告" [tgbot.answers] "successfulOperation" = "✅ 成功!" "errorOperation" = "❗ 操作错误。" "getInboundsFailed" = "❌ 获取入站信息失败。" "getClientsFailed" = "❌ 获取客户失败。" "canceled" = "❌ {{ .Email }}:操作已取消。" "clientRefreshSuccess" = "✅ {{ .Email }}:客户端刷新成功。" "IpRefreshSuccess" = "✅ {{ .Email }}:IP 刷新成功。" "TGIdRefreshSuccess" = "✅ {{ .Email }}:客户端的 Telegram 用户刷新成功。" "resetTrafficSuccess" = "✅ {{ .Email }}:流量已重置成功。" "setTrafficLimitSuccess" = "✅ {{ .Email }}: 流量限制保存成功。" "expireResetSuccess" = "✅ {{ .Email }}:过期天数已重置成功。" "resetIpSuccess" = "✅ {{ .Email }}:成功保存 IP 限制数量为 {{ .Count }}。" "clearIpSuccess" = "✅ {{ .Email }}:IP 已成功清除。" "getIpLog" = "✅ {{ .Email }}:获取 IP 日志。" "getUserInfo" = "✅ {{ .Email }}:获取 Telegram 用户信息。" "removedTGUserSuccess" = "✅ {{ .Email }}:Telegram 用户已成功移除。" "enableSuccess" = "✅ {{ .Email }}:已成功启用。" "disableSuccess" = "✅ {{ .Email }}:已成功禁用。" "askToAddUserId" = "未找到您的配置!\r\n请向管理员询问,在您的配置中使用您的 Telegram 用户 ChatID。\r\n\r\n您的用户 ChatID:{{ .TgUserID }}" "chooseClient" = "为入站 {{ .Inbound }} 选择一个客户" "chooseInbound" = "选择一个入站" ================================================ FILE: web/translation/translate.zh_TW.toml ================================================ "username" = "使用者名稱" "password" = "密碼" "login" = "登入" "confirm" = "確定" "cancel" = "取消" "close" = "關閉" "create" = "建立" "update" = "更新" "copy" = "複製" "copied" = "已複製" "download" = "下載" "remark" = "備註" "enable" = "啟用" "protocol" = "協議" "search" = "搜尋" "filter" = "篩選" "loading" = "載入中..." "second" = "秒" "minute" = "分鐘" "hour" = "小時" "day" = "天" "check" = "檢視" "indefinite" = "無限期" "unlimited" = "無限制" "none" = "無" "qrCode" = "二維碼" "info" = "更多資訊" "edit" = "編輯" "delete" = "刪除" "reset" = "重置" "noData" = "無數據。" "copySuccess" = "複製成功" "sure" = "確定" "encryption" = "加密" "useIPv4ForHost" = "使用 IPv4 連接主機" "transmission" = "傳輸" "host" = "主機" "path" = "路徑" "camouflage" = "偽裝" "status" = "狀態" "enabled" = "開啟" "disabled" = "關閉" "depleted" = "耗盡" "depletingSoon" = "即將耗盡" "offline" = "離線" "online" = "線上" "domainName" = "域名" "monitor" = "監聽" "certificate" = "憑證" "fail" = "失敗" "comment" = "評論" "success" = "成功" "lastOnline" = "上次上線" "getVersion" = "獲取版本" "install" = "安裝" "clients" = "客戶端" "usage" = "使用情況" "twoFactorCode" = "代碼" "remained" = "剩餘" "security" = "安全" "secAlertTitle" = "安全警報" "secAlertSsl" = "此連線不安全。在啟用 TLS 進行資料保護之前,請勿輸入敏感資訊。" "secAlertConf" = "某些設定易受攻擊。建議加強安全協議以防止潛在漏洞。" "secAlertSSL" = "面板缺少安全連線。請安裝 TLS 證書以保護資料安全。" "secAlertPanelPort" = "面板預設埠存在安全風險。請配置隨機埠或特定埠。" "secAlertPanelURI" = "面板預設 URI 路徑不安全。請配置複雜的 URI 路徑。" "secAlertSubURI" = "訂閱預設 URI 路徑不安全。請配置複雜的 URI 路徑。" "secAlertSubJsonURI" = "訂閱 JSON 預設 URI 路徑不安全。請配置複雜的 URI 路徑。" "emptyDnsDesc" = "未添加DNS伺服器。" "emptyFakeDnsDesc" = "未添加Fake DNS伺服器。" "emptyBalancersDesc" = "未添加負載平衡器。" "emptyReverseDesc" = "未添加反向代理。" "somethingWentWrong" = "發生錯誤" [subscription] "title" = "訂閱資訊" "subId" = "訂閱 ID" "status" = "狀態" "downloaded" = "已下載" "uploaded" = "已上傳" "expiry" = "到期" "totalQuota" = "總配額" "individualLinks" = "個別連結" "active" = "啟用" "inactive" = "停用" "unlimited" = "無限制" "noExpiry" = "無到期" [menu] "theme" = "主題" "dark" = "深色" "ultraDark" = "超深色" "dashboard" = "系統狀態" "inbounds" = "入站列表" "settings" = "面板設定" "xray" = "Xray 設定" "logout" = "退出登入" "link" = "管理" [pages.login] "hello" = "你好" "title" = "歡迎" "loginAgain" = "登入時效已過,請重新登入" [pages.login.toasts] "invalidFormData" = "資料格式錯誤" "emptyUsername" = "請輸入使用者名稱" "emptyPassword" = "請輸入密碼" "wrongUsernameOrPassword" = "用戶名、密碼或雙重驗證碼無效。" "successLogin" = "您已成功登入您的帳戶。" [pages.index] "title" = "系統狀態" "cpu" = "CPU" "logicalProcessors" = "邏輯處理器" "frequency" = "頻率" "swap" = "交換空間" "storage" = "儲存" "memory" = "記憶體" "threads" = "執行緒" "xrayStatus" = "Xray" "stopXray" = "停止" "restartXray" = "重啟" "xraySwitch" = "版本" "xraySwitchClick" = "選擇你要切換到的版本" "xraySwitchClickDesk" = "請謹慎選擇,因為較舊版本可能與當前配置不相容" "xrayStatusUnknown" = "未知" "xrayStatusRunning" = "運行中" "xrayStatusStop" = "停止" "xrayStatusError" = "錯誤" "xrayErrorPopoverTitle" = "執行Xray時發生錯誤" "operationHours" = "系統正常執行時間" "systemLoad" = "系統負載" "systemLoadDesc" = "過去 1、5 和 15 分鐘的系統平均負載" "connectionCount" = "連線數" "ipAddresses" = "IP地址" "toggleIpVisibility" = "切換IP可見性" "overallSpeed" = "整體速度" "upload" = "上傳" "download" = "下載" "totalData" = "總數據" "sent" = "已發送" "received" = "已接收" "documentation" = "文件" "xraySwitchVersionDialog" = "您確定要變更Xray版本嗎?" "xraySwitchVersionDialogDesc" = "這將會把Xray版本變更為#version#。" "xraySwitchVersionPopover" = "Xray 更新成功" "geofileUpdateDialog" = "您確定要更新地理檔案嗎?" "geofileUpdateDialogDesc" = "這將更新 #filename# 檔案。" "geofilesUpdateDialogDesc" = "這將更新所有文件。" "geofilesUpdateAll" = "全部更新" "geofileUpdatePopover" = "地理檔案更新成功" "dontRefresh" = "安裝中,請勿重新整理此頁面" "logs" = "日誌" "config" = "配置" "backup" = "備份和恢復" "backupTitle" = "備份和恢復資料庫" "exportDatabase" = "備份" "exportDatabaseDesc" = "點擊下載包含當前資料庫備份的 .db 文件到您的設備。" "importDatabase" = "恢復" "importDatabaseDesc" = "點擊選擇並上傳設備中的 .db 文件以從備份恢復資料庫。" "importDatabaseSuccess" = "資料庫匯入成功" "importDatabaseError" = "匯入資料庫時發生錯誤" "readDatabaseError" = "讀取資料庫時發生錯誤" "getDatabaseError" = "檢索資料庫時發生錯誤" "getConfigError" = "檢索設定檔時發生錯誤" [pages.inbounds] "allTimeTraffic" = "累計總流量" "allTimeTrafficUsage" = "所有时间总使用量" "title" = "入站列表" "totalDownUp" = "總上傳 / 下載" "totalUsage" = "總用量" "inboundCount" = "入站數量" "operate" = "選單" "enable" = "啟用" "remark" = "備註" "protocol" = "協議" "port" = "埠" "portMap" = "埠映射" "traffic" = "流量" "details" = "詳細資訊" "transportConfig" = "傳輸配置" "expireDate" = "到期時間" "createdAt" = "建立時間" "updatedAt" = "更新時間" "resetTraffic" = "重置流量" "addInbound" = "新增入站" "generalActions" = "通用操作" "autoRefresh" = "自動刷新" "autoRefreshInterval" = "間隔" "modifyInbound" = "修改入站" "deleteInbound" = "刪除入站" "deleteInboundContent" = "確定要刪除入站嗎?" "deleteClient" = "刪除客戶端" "deleteClientContent" = "確定要刪除客戶端嗎?" "resetTrafficContent" = "確定要重置流量嗎?" "copyLink" = "複製連結" "address" = "地址" "network" = "網路" "destinationPort" = "目標埠" "targetAddress" = "目標地址" "monitorDesc" = "留空表示監聽所有 IP" "meansNoLimit" = "= 無限制(單位:GB)" "totalFlow" = "總流量" "leaveBlankToNeverExpire" = "留空表示永不過期" "noRecommendKeepDefault" = "建議保留預設值" "certificatePath" = "檔案路徑" "certificateContent" = "檔案內容" "publicKey" = "公鑰" "privatekey" = "私鑰" "clickOnQRcode" = "點選二維碼複製" "client" = "客戶" "export" = "匯出連結" "clone" = "複製" "cloneInbound" = "複製" "cloneInboundContent" = "此入站規則除埠(Port)、監聽 IP(Listening IP)和客戶端(Clients)以外的所有配置都將應用於克隆" "cloneInboundOk" = "建立克隆" "resetAllTraffic" = "重置所有入站流量" "resetAllTrafficTitle" = "重置所有入站流量" "resetAllTrafficContent" = "確定要重置所有入站流量嗎?" "resetInboundClientTraffics" = "重置客戶端流量" "resetInboundClientTrafficTitle" = "重置所有客戶端流量" "resetInboundClientTrafficContent" = "確定要重置此入站客戶端的所有流量嗎?" "resetAllClientTraffics" = "重置所有客戶端流量" "resetAllClientTrafficTitle" = "重置所有客戶端流量" "resetAllClientTrafficContent" = "確定要重置所有客戶端的所有流量嗎?" "delDepletedClients" = "刪除流量耗盡的客戶端" "delDepletedClientsTitle" = "刪除流量耗盡的客戶端" "delDepletedClientsContent" = "確定要刪除所有流量耗盡的客戶端嗎?" "email" = "電子郵件" "emailDesc" = "電子郵件必須完全唯一" "IPLimit" = "IP 限制" "IPLimitDesc" = "如果數量超過設定值,則禁用入站流量。(0 = 禁用)" "IPLimitlog" = "IP 日誌" "IPLimitlogDesc" = "IP 歷史日誌(要啟用被禁用的入站流量,請清除日誌)" "IPLimitlogclear" = "清除日誌" "setDefaultCert" = "從面板設定證書" "telegramDesc" = "請提供Telegram聊天ID。(在機器人中使用'/id'命令)或(@userinfobot" "subscriptionDesc" = "要找到你的訂閱 URL,請導航到“詳細資訊”。此外,你可以為多個客戶端使用相同的名稱。" "info" = "資訊" "same" = "相同" "inboundData" = "入站資料" "exportInbound" = "匯出入站規則" "import" = "匯入" "importInbound" = "匯入入站規則" "periodicTrafficResetTitle" = "流量重置" "periodicTrafficResetDesc" = "按指定間隔自動重置流量計數器" "lastReset" = "上次重置" [pages.client] "add" = "新增客戶端" "edit" = "編輯客戶端" "submitAdd" = "新增客戶端" "submitEdit" = "儲存修改" "clientCount" = "客戶端數量" "bulk" = "批量建立" "method" = "方法" "first" = "置頂" "last" = "置底" "prefix" = "字首" "postfix" = "字尾" "delayedStart" = "首次使用後開始" "expireDays" = "期間" "days" = "天" "renew" = "自動續訂" "renewDesc" = "到期後自動續訂。(0 = 禁用)(單位: 天)" [pages.inbounds.periodicTrafficReset] "never" = "從不" "daily" = "每日" "weekly" = "每週" "monthly" = "每月" [pages.inbounds.toasts] "obtain" = "獲取" "updateSuccess" = "更新成功" "logCleanSuccess" = "日誌已清除" "inboundsUpdateSuccess" = "入站連接已成功更新" "inboundUpdateSuccess" = "入站連接已成功更新" "inboundCreateSuccess" = "入站連接已成功建立" "inboundDeleteSuccess" = "入站連接已成功刪除" "inboundClientAddSuccess" = "已新增入站客戶端" "inboundClientDeleteSuccess" = "入站客戶端已刪除" "inboundClientUpdateSuccess" = "入站客戶端已更新" "delDepletedClientsSuccess" = "所有耗盡客戶端已刪除" "resetAllClientTrafficSuccess" = "客戶端所有流量已重置" "resetAllTrafficSuccess" = "所有流量已重置" "resetInboundClientTrafficSuccess" = "流量已重置" "trafficGetError" = "取得流量資料時發生錯誤" "getNewX25519CertError" = "取得X25519憑證時發生錯誤。" "getNewmldsa65Error" = "取得mldsa65憑證時發生錯誤。" "getNewVlessEncError" = "取得VlessEnc憑證時發生錯誤。" [pages.inbounds.stream.general] "request" = "請求" "response" = "響應" "name" = "名稱" "value" = "值" [pages.inbounds.stream.tcp] "version" = "版本" "method" = "方法" "path" = "路徑" "status" = "狀態" "statusDescription" = "狀態說明" "requestHeader" = "請求頭" "responseHeader" = "響應頭" [pages.settings] "title" = "面板設定" "save" = "儲存" "infoDesc" = "此處的所有更改都需要儲存並重啟面板才能生效" "restartPanel" = "重啟面板" "restartPanelDesc" = "確定要重啟面板嗎?若重啟後無法訪問面板,請前往伺服器檢視面板日誌資訊" "restartPanelSuccess" = "面板已成功重新啟動" "actions" = "操作" "resetDefaultConfig" = "重置為預設配置" "panelSettings" = "常規" "securitySettings" = "安全設定" "TGBotSettings" = "Telegram 機器人配置" "panelListeningIP" = "面板監聽 IP" "panelListeningIPDesc" = "預設留空監聽所有 IP" "panelListeningDomain" = "面板監聽域名" "panelListeningDomainDesc" = "預設情況下留空以監視所有域名和 IP 地址" "panelPort" = "面板監聽埠" "panelPortDesc" = "重啟面板生效" "publicKeyPath" = "面板證書公鑰檔案路徑" "publicKeyPathDesc" = "填寫一個 '/' 開頭的絕對路徑" "privateKeyPath" = "面板證書金鑰檔案路徑" "privateKeyPathDesc" = "填寫一個 '/' 開頭的絕對路徑" "panelUrlPath" = "面板 url 根路徑" "panelUrlPathDesc" = "必須以 '/' 開頭,以 '/' 結尾" "pageSize" = "分頁大小" "pageSizeDesc" = "定義入站表的頁面大小。設定 0 表示禁用" "remarkModel" = "備註模型和分隔符" "datepicker" = "日期選擇器" "datepickerPlaceholder" = "選擇日期" "datepickerDescription" = "選擇器日曆類型指定到期日期" "sampleRemark" = "備註示例" "oldUsername" = "原使用者名稱" "currentPassword" = "原密碼" "newUsername" = "新使用者名稱" "newPassword" = "新密碼" "telegramBotEnable" = "啟用 Telegram 機器人" "telegramBotEnableDesc" = "啟用 Telegram 機器人功能" "telegramToken" = "Telegram 機器人令牌(token)" "telegramTokenDesc" = "從 '@BotFather' 獲取的 Telegram 機器人令牌" "telegramProxy" = "SOCKS5 Proxy" "telegramProxyDesc" = "啟用 SOCKS5 代理連線到 Telegram(根據指南調整設定)" "telegramAPIServer" = "Telegram API Server" "telegramAPIServerDesc" = "要使用的 Telegram API 伺服器。留空以使用預設伺服器。" "telegramChatId" = "管理員聊天 ID" "telegramChatIdDesc" = "Telegram 管理員聊天 ID (多個以逗號分隔)(可通過 @userinfobot 獲取,或在機器人中使用 '/id' 命令獲取)" "telegramNotifyTime" = "通知時間" "telegramNotifyTimeDesc" = "設定週期性的 Telegram 機器人通知時間(使用 crontab 時間格式)" "tgNotifyBackup" = "資料庫備份" "tgNotifyBackupDesc" = "傳送帶有報告的資料庫備份檔案" "tgNotifyLogin" = "登入通知" "tgNotifyLoginDesc" = "當有人試圖登入你的面板時顯示使用者名稱、IP 地址和時間" "sessionMaxAge" = "會話時長" "sessionMaxAgeDesc" = "保持登入狀態的時長(單位:分鐘)" "expireTimeDiff" = "到期通知閾值" "expireTimeDiffDesc" = "達到此閾值時,將收到有關到期時間的通知(單位:天)" "trafficDiff" = "流量耗盡閾值" "trafficDiffDesc" = "達到此閾值時,將收到有關流量耗盡的通知(單位:GB)" "tgNotifyCpu" = "CPU 負載通知閾值" "tgNotifyCpuDesc" = "CPU 負載超過此閾值時,將收到通知(單位:%)" "timeZone" = "時區" "timeZoneDesc" = "定時任務將按照該時區的時間執行" "subSettings" = "訂閱設定" "subEnable" = "啟用訂閱服務" "subEnableDesc" = "啟用訂閱服務功能" "subJsonEnable" = "獨立啟用/停用 JSON 訂閱端點。" "subTitle" = "訂閱標題" "subTitleDesc" = "在VPN客戶端中顯示的標題" "subSupportUrl" = "支援連結" "subSupportUrlDesc" = "VPN 用戶端中顯示的技術支援連結" "subProfileUrl" = "個人資料連結" "subProfileUrlDesc" = "VPN 用戶端中顯示的網站連結" "subAnnounce" = "公告" "subAnnounceDesc" = "VPN 用戶端中顯示的公告文字" "subEnableRouting" = "啟用路由" "subEnableRoutingDesc" = "在 VPN 用戶端中啟用路由的全域設定。(僅限 Happ)" "subRoutingRules" = "路由規則" "subRoutingRulesDesc" = "VPN 用戶端的全域路由規則。(僅限 Happ)" "subListen" = "監聽 IP" "subListenDesc" = "訂閱服務監聽的 IP 地址(留空表示監聽所有 IP)" "subPort" = "監聽埠" "subPortDesc" = "訂閱服務監聽的埠號(必須是未使用的埠)" "subCertPath" = "公鑰路徑" "subCertPathDesc" = "訂閱服務使用的公鑰檔案路徑(以 '/' 開頭)" "subKeyPath" = "私鑰路徑" "subKeyPathDesc" = "訂閱服務使用的私鑰檔案路徑(以 '/' 開頭)" "subPath" = "URI 路徑" "subPathDesc" = "訂閱服務使用的 URI 路徑(以 '/' 開頭,以 '/' 結尾)" "subDomain" = "監聽域名" "subDomainDesc" = "訂閱服務監聽的域名(留空表示監聽所有域名和 IP)" "subUpdates" = "更新間隔" "subUpdatesDesc" = "客戶端應用中訂閱 URL 的更新間隔(單位:小時)" "subEncrypt" = "編碼" "subEncryptDesc" = "訂閱服務返回的內容將採用 Base64 編碼" "subShowInfo" = "顯示使用資訊" "subShowInfoDesc" = "客戶端應用中將顯示剩餘流量和日期資訊" "subURI" = "反向代理 URI" "subURIDesc" = "用於代理後面的訂閱 URL 的 URI 路徑" "externalTrafficInformEnable" = "外部交通通知" "externalTrafficInformEnableDesc" = "每次流量更新時通知外部 API" "externalTrafficInformURI" = "外部流量通知 URI" "externalTrafficInformURIDesc" = "流量更新將會傳送到此 URI" "fragment" = "分片" "fragmentDesc" = "啟用 TLS hello 資料包分片" "fragmentSett" = "設定" "noisesDesc" = "啟用 Noises." "noisesSett" = "Noises 設定" "mux" = "多路複用器" "muxDesc" = "在已建立的資料流內傳輸多個獨立的資料流" "muxSett" = "複用器設定" "direct" = "直接連線" "directDesc" = "直接與特定國家的域或IP範圍建立連線" "notifications" = "通知" "certs" = "證書" "externalTraffic" = "外部流量" "dateAndTime" = "日期和時間" "proxyAndServer" = "代理和伺服器" "intervals" = "間隔" "information" = "資訊" "language" = "語言" "telegramBotLanguage" = "Telegram 機器人語言" [pages.xray] "title" = "Xray 配置" "save" = "儲存" "restart" = "重新啟動 Xray" "restartSuccess" = "Xray 已成功重新啟動" "stopSuccess" = "Xray 已成功停止" "restartError" = "重新啟動Xray時發生錯誤。" "stopError" = "停止Xray時發生錯誤。" "basicTemplate" = "基礎配置" "advancedTemplate" = "高階配置" "generalConfigs" = "常規配置" "generalConfigsDesc" = "這些選項將決定常規配置" "logConfigs" = "日誌" "logConfigsDesc" = "日誌可能會影響伺服器的效能,建議僅在需要時啟用" "blockConfigsDesc" = "這些選項將阻止使用者連線到特定協議和網站" "basicRouting" = "基本路由" "blockConnectionsConfigsDesc" = "這些選項將根據特定的請求國家阻止流量。" "directConnectionsConfigsDesc" = "直接連線確保特定的流量不會通過其他伺服器路由。" "blockips" = "阻止IP" "blockdomains" = "阻止域名" "directips" = "直接IP" "directdomains" = "直接域名" "ipv4Routing" = "IPv4 路由" "ipv4RoutingDesc" = "此選項將僅通過 IPv4 路由到目標域" "warpRouting" = "WARP 路由" "warpRoutingDesc" = "注意:在使用這些選項之前,請按照面板 GitHub 上的步驟在你的伺服器上以 socks5 代理模式安裝 WARP。WARP 將通過 Cloudflare 伺服器將流量路由到網站。" "Template" = "高階 Xray 配置模板" "TemplateDesc" = "最終的 Xray 配置檔案將基於此模板生成" "FreedomStrategy" = "Freedom 協議策略" "FreedomStrategyDesc" = "設定 Freedom 協議中網路的輸出策略" "RoutingStrategy" = "配置路由域策略" "RoutingStrategyDesc" = "設定 DNS 解析的整體路由策略" "outboundTestUrl" = "出站測試 URL" "outboundTestUrlDesc" = "測試出站連線時使用的 URL" "Torrent" = "遮蔽 BitTorrent 協議" "Inbounds" = "入站規則" "InboundsDesc" = "接受來自特定客戶端的流量" "Outbounds" = "出站規則" "Balancers" = "負載均衡" "OutboundsDesc" = "設定出站流量傳出方式" "Routings" = "路由規則" "RoutingsDesc" = "每條規則的優先順序都很重要" "completeTemplate" = "全部" "logLevel" = "日誌級別" "logLevelDesc" = "錯誤日誌的日誌級別,用於指示需要記錄的資訊" "accessLog" = "訪問日誌" "accessLogDesc" = "訪問日誌的檔案路徑。特殊值 'none' 禁用訪問日誌" "errorLog" = "錯誤日誌" "errorLogDesc" = "錯誤日誌的檔案路徑。特殊值 'none' 禁用錯誤日誌" "dnsLog" = "DNS 日誌" "dnsLogDesc" = "是否啟用 DNS 查詢日誌" "maskAddress" = "隱藏地址" "maskAddressDesc" = "IP 地址掩碼,啟用時會自動替換日誌中出現的 IP 地址。" "statistics" = "統計" "statsInboundUplink" = "入站上傳統計" "statsInboundUplinkDesc" = "啟用所有入站代理的上行流量統計收集。" "statsInboundDownlink" = "入站下載統計" "statsInboundDownlinkDesc" = "啟用所有入站代理的下行流量統計收集。" "statsOutboundUplink" = "出站上傳統計" "statsOutboundUplinkDesc" = "啟用所有出站代理的上行流量統計收集。" "statsOutboundDownlink" = "出站下載統計" "statsOutboundDownlinkDesc" = "啟用所有出站代理的下行流量統計收集。" [pages.xray.rules] "first" = "置頂" "last" = "置底" "up" = "向上" "down" = "向下" "source" = "來源" "dest" = "目的地址" "inbound" = "入站" "outbound" = "出站" "balancer" = "負載均衡" "info" = "資訊" "add" = "新增規則" "edit" = "編輯規則" "useComma" = "逗號分隔的項目" [pages.xray.outbound] "addOutbound" = "新增出站" "addReverse" = "新增反向" "editOutbound" = "編輯出站" "editReverse" = "編輯反向" "tag" = "標籤" "tagDesc" = "唯一標籤" "address" = "地址" "reverse" = "反向" "domain" = "域名" "type" = "類型" "bridge" = "Bridge" "portal" = "Portal" "link" = "連結" "intercon" = "互連" "settings" = "設定" "accountInfo" = "帳戶資訊" "outboundStatus" = "出站狀態" "sendThrough" = "傳送通過" "test" = "測試" "testResult" = "測試結果" "testing" = "正在測試連接..." "testSuccess" = "測試成功" "testFailed" = "測試失敗" "testError" = "測試出站失敗" [pages.xray.balancer] "addBalancer" = "新增負載均衡" "editBalancer" = "編輯負載均衡" "balancerStrategy" = "策略" "balancerSelectors" = "選擇器" "tag" = "標籤" "tagDesc" = "唯一標籤" "balancerDesc" = "無法同時使用 balancerTag 和 outboundTag。如果同時使用,則只有 outboundTag 會生效。" [pages.xray.wireguard] "secretKey" = "金鑰" "publicKey" = "公鑰" "allowedIPs" = "允許的 IP" "endpoint" = "端點" "psk" = "共享金鑰" "domainStrategy" = "域策略" [pages.xray.tun] "nameDesc" = "TUN 介面的名稱。預設值為 'xray0'" "mtuDesc" = "最大傳輸單元。資料包的最大大小。預設值為 1500" "userLevel" = "用戶級別" "userLevelDesc" = "通過此入站的所有連接都將使用此用戶級別。預設值為 0" [pages.xray.dns] "enable" = "啟用 DNS" "enableDesc" = "啟用內建 DNS 伺服器" "tag" = "DNS 入站標籤" "tagDesc" = "此標籤將在路由規則中可用作入站標籤" "clientIp" = "客戶端IP" "clientIpDesc" = "用於在DNS查詢期間通知伺服器指定的IP位置" "disableCache" = "禁用快取" "disableCacheDesc" = "禁用DNS快取" "disableFallback" = "禁用回退" "disableFallbackDesc" = "禁用回退DNS查詢" "disableFallbackIfMatch" = "匹配時禁用回退" "disableFallbackIfMatchDesc" = "當DNS伺服器的匹配域名列表命中時,禁用回退DNS查詢" "enableParallelQuery" = "啟用並行查詢" "enableParallelQueryDesc" = "啟用並行DNS查詢到多個伺服器以實現更快的解析" "strategy" = "查詢策略" "strategyDesc" = "解析域名的總體策略" "add" = "新增伺服器" "edit" = "編輯伺服器" "domains" = "域" "expectIPs" = "預期 IP" "unexpectIPs" = "意外IP" "useSystemHosts" = "使用系統Hosts" "useSystemHostsDesc" = "使用已安裝系統的hosts檔案" "usePreset" = "使用範本" "dnsPresetTitle" = "DNS範本" "dnsPresetFamily" = "家庭" [pages.xray.fakedns] "add" = "新增假 DNS" "edit" = "編輯假 DNS" "ipPool" = "IP 池子網" "poolSize" = "池大小" [pages.settings.security] "admin" = "管理員憑證" "twoFactor" = "雙重驗證" "twoFactorEnable" = "啟用2FA" "twoFactorEnableDesc" = "增加額外的驗證層以提高安全性。" "twoFactorModalSetTitle" = "啟用雙重認證" "twoFactorModalDeleteTitle" = "停用雙重認證" "twoFactorModalSteps" = "要設定雙重認證,請執行以下步驟:" "twoFactorModalFirstStep" = "1. 在認證應用程式中掃描此QR碼,或複製QR碼附近的令牌並貼到應用程式中" "twoFactorModalSecondStep" = "2. 輸入應用程式中的驗證碼" "twoFactorModalRemoveStep" = "輸入應用程式中的驗證碼以移除雙重認證。" "twoFactorModalChangeCredentialsTitle" = "更改憑證" "twoFactorModalChangeCredentialsStep" = "輸入應用程式中的代碼以更改管理員憑證。" "twoFactorModalSetSuccess" = "雙重身份驗證已成功建立" "twoFactorModalDeleteSuccess" = "雙重身份驗證已成功刪除" "twoFactorModalError" = "驗證碼錯誤" [pages.settings.toasts] "modifySettings" = "參數已更改。" "getSettings" = "取得參數時發生錯誤" "modifyUserError" = "變更管理員憑證時發生錯誤。" "modifyUser" = "您已成功變更管理員憑證。" "originalUserPassIncorrect" = "原使用者名稱或原密碼錯誤" "userPassMustBeNotEmpty" = "新使用者名稱和新密碼不能為空" "getOutboundTrafficError" = "取得出站流量錯誤" "resetOutboundTrafficError" = "重設出站流量錯誤" [tgbot] "keyboardClosed" = "❌ 自定義鍵盤已關閉!" "noResult" = "❗ 沒有結果!" "noQuery" = "❌ 未找到查詢!請再次使用該命令!" "wentWrong" = "❌ 出了點問題!" "noIpRecord" = "❗ 沒有IP記錄!" "noInbounds" = "❗ 未找到入站!" "unlimited" = "♾ 無限(重置)" "add" = "添加" "month" = "月" "months" = "月" "day" = "天" "days" = "天" "hours" = "小時" "minutes" = "分鐘" "unknown" = "未知" "inbounds" = "入站" "clients" = "客戶端" "offline" = "🔴 離線" "online" = "🟢 在線" [tgbot.commands] "unknown" = "❗ 未知命令" "pleaseChoose" = "👇 請選擇:\r\n" "help" = "🤖 歡迎使用本機器人!它旨在為您提供來自伺服器的特定資料,並允許您進行必要的修改。\r\n\r\n" "start" = "👋 你好,{{ .Firstname }}。\r\n" "welcome" = "🤖 歡迎來到 {{ .Hostname }} 管理機器人。\r\n" "status" = "✅ 機器人正常執行!" "usage" = "❗ 請輸入要搜尋的文字!" "getID" = "🆔 您的 ID 為:{{ .ID }}" "helpAdminCommands" = "要重新啟動 Xray Core:\r\n/restart\r\n\r\n要搜尋客戶電子郵件:\r\n/usage [電子郵件]\r\n\r\n要搜尋入站(帶有客戶統計資料):\r\n/inbound [備註]\r\n\r\nTelegram聊天ID:\r\n/id" "helpClientCommands" = "要搜尋統計資料,請使用以下命令:\r\n/usage [電子郵件]\r\n\r\nTelegram聊天ID:\r\n/id" "restartUsage" = "\r\n\r\n/restart" "restartSuccess" = "✅ 操作成功!" "restartFailed" = "❗ 操作錯誤。\r\n\r\n錯誤: {{ .Error }}." "xrayNotRunning" = "❗ Xray Core 未運行。" "startDesc" = "顯示主選單" "helpDesc" = "機器人幫助" "statusDesc" = "檢查機器人狀態" "idDesc" = "顯示您的 Telegram ID" [tgbot.messages] "cpuThreshold" = "🔴 CPU 使用率為 {{ .Percent }}%,超過閾值 {{ .Threshold }}%" "selectUserFailed" = "❌ 使用者選擇錯誤!" "userSaved" = "✅ 電報使用者已儲存。" "loginSuccess" = "✅ 成功登入到面板。\r\n" "loginFailed" = "❗️ 面板登入失敗。\r\n" "2faFailed" = "2FA 失敗" "report" = "🕰 定時報告:{{ .RunTime }}\r\n" "datetime" = "⏰ 日期時間:{{ .DateTime }}\r\n" "hostname" = "💻 主機名:{{ .Hostname }}\r\n" "version" = "🚀 X-UI 版本:{{ .Version }}\r\n" "xrayVersion" = "📡 Xray 版本: {{ .XrayVersion }}\r\n" "ipv6" = "🌐 IPv6:{{ .IPv6 }}\r\n" "ipv4" = "🌐 IPv4:{{ .IPv4 }}\r\n" "ip" = "🌐 IP:{{ .IP }}\r\n" "ips" = "🔢 IP 地址:\r\n{{ .IPs }}\r\n" "serverUpTime" = "⏳ 伺服器執行時間:{{ .UpTime }} {{ .Unit }}\r\n" "serverLoad" = "📈 伺服器負載:{{ .Load1 }}, {{ .Load2 }}, {{ .Load3 }}\r\n" "serverMemory" = "📋 伺服器記憶體:{{ .Current }}/{{ .Total }}\r\n" "tcpCount" = "🔹 TCP 連線數:{{ .Count }}\r\n" "udpCount" = "🔸 UDP 連線數:{{ .Count }}\r\n" "traffic" = "🚦 流量:{{ .Total }} (↑{{ .Upload }},↓{{ .Download }})\r\n" "xrayStatus" = "ℹ️ Xray 狀態:{{ .State }}\r\n" "username" = "👤 使用者名稱:{{ .Username }}\r\n" "password" = "👤 密碼: {{ .Password }}\r\n" "time" = "⏰ 時間:{{ .Time }}\r\n" "inbound" = "📍 入站:{{ .Remark }}\r\n" "port" = "🔌 埠:{{ .Port }}\r\n" "expire" = "📅 過期日期:{{ .Time }}\r\n" "expireIn" = "📅 剩餘時間:{{ .Time }}\r\n" "active" = "💡 啟用:{{ .Enable }}\r\n" "enabled" = "🚨 已啟用:{{ .Enable }}\r\n" "online" = "🌐 連線狀態:{{ .Status }}\r\n" "lastOnline" = "🔙 上次上線: {{ .Time }}\r\n" "email" = "📧 郵箱:{{ .Email }}\r\n" "upload" = "🔼 上傳↑:{{ .Upload }}\r\n" "download" = "🔽 下載↓:{{ .Download }}\r\n" "total" = "📊 總計:{{ .UpDown }} / {{ .Total }}\r\n" "TGUser" = "👤 電報使用者:{{ .TelegramID }}\r\n" "exhaustedMsg" = "🚨 耗盡的 {{ .Type }}:\r\n" "exhaustedCount" = "🚨 耗盡的 {{ .Type }} 數量:\r\n" "onlinesCount" = "🌐 線上客戶:{{ .Count }}\r\n" "disabled" = "🛑 禁用:{{ .Disabled }}\r\n" "depleteSoon" = "🔜 即將耗盡:{{ .Deplete }}\r\n\r\n" "backupTime" = "🗄 備份時間:{{ .Time }}\r\n" "refreshedOn" = "\r\n📋🔄 重新整理時間:{{ .Time }}\r\n\r\n" "yes" = "✅ 是的" "no" = "❌ 沒有" "received_id" = "🔑📥 ID 已更新。" "received_password" = "🔑📥 密碼已更新。" "received_email" = "📧📥 電子郵件已更新。" "received_comment" = "💬📥 評論已更新。" "id_prompt" = "🔑 預設 ID: {{ .ClientId }}\n\n請輸入您的 ID。" "pass_prompt" = "🔑 預設密碼: {{ .ClientPassword }}\n\n請輸入您的密碼。" "email_prompt" = "📧 預設電子郵件: {{ .ClientEmail }}\n\n請輸入您的電子郵件。" "comment_prompt" = "💬 預設評論: {{ .ClientComment }}\n\n請輸入您的評論。" "inbound_client_data_id" = "🔄 入站: {{ .InboundRemark }}\n\n🔑 ID: {{ .ClientId }}\n📧 電子郵件: {{ .ClientEmail }}\n📊 流量: {{ .ClientTraffic }}\n📅 到期日: {{ .ClientExp }}\n🌐 IP 限制: {{ .IpLimit }}\n💬 備註: {{ .ClientComment }}\n\n你現在可以將客戶加入入站了!" "inbound_client_data_pass" = "🔄 入站: {{ .InboundRemark }}\n\n🔑 密碼: {{ .ClientPass }}\n📧 電子郵件: {{ .ClientEmail }}\n📊 流量: {{ .ClientTraffic }}\n📅 到期日: {{ .ClientExp }}\n🌐 IP 限制: {{ .IpLimit }}\n💬 備註: {{ .ClientComment }}\n\n你現在可以將客戶加入入站了!" "cancel" = "❌ 程序已取消!\n\n您可以隨時使用 /start 重新開始。 🔄" "error_add_client" = "⚠️ 錯誤:\n\n {{ .error }}" "using_default_value" = "好的,我會使用預設值。 😊" "incorrect_input" = "您的輸入無效。\n短語應連續輸入,不能有空格。\n正確示例: aaaaaa\n錯誤示例: aaa aaa 🚫" "AreYouSure" = "你確定嗎?🤔" "SuccessResetTraffic" = "📧 電子郵件: {{ .ClientEmail }}\n🏁 結果: ✅ 成功" "FailedResetTraffic" = "📧 電子郵件: {{ .ClientEmail }}\n🏁 結果: ❌ 失敗 \n\n🛠️ 錯誤: [ {{ .ErrorMessage }} ]" "FinishProcess" = "🔚 所有客戶的流量重置已完成。" [tgbot.buttons] "closeKeyboard" = "❌ 關閉鍵盤" "cancel" = "❌ 取消" "cancelReset" = "❌ 取消重置" "cancelIpLimit" = "❌ 取消 IP 限制" "confirmResetTraffic" = "✅ 確認重置流量?" "confirmClearIps" = "✅ 確認清除 IP?" "confirmRemoveTGUser" = "✅ 確認移除 Telegram 使用者?" "confirmToggle" = "✅ 確認啟用/禁用使用者?" "dbBackup" = "獲取資料庫備份" "serverUsage" = "伺服器使用情況" "getInbounds" = "獲取入站資訊" "depleteSoon" = "即將耗盡" "clientUsage" = "獲取使用情況" "onlines" = "線上客戶端" "commands" = "命令" "refresh" = "🔄 重新整理" "clearIPs" = "❌ 清除 IP" "removeTGUser" = "❌ 移除 Telegram 使用者" "selectTGUser" = "👤 選擇 Telegram 使用者" "selectOneTGUser" = "👤 選擇一個 Telegram 使用者:" "resetTraffic" = "📈 重置流量" "resetExpire" = "📅 更改到期日期" "ipLog" = "🔢 IP 日誌" "ipLimit" = "🔢 IP 限制" "setTGUser" = "👤 設定 Telegram 使用者" "toggle" = "🔘 啟用/禁用" "custom" = "🔢 風俗" "confirmNumber" = "✅ 確認: {{ .Num }}" "confirmNumberAdd" = "✅ 確認新增:{{ .Num }}" "limitTraffic" = "🚧 流量限制" "getBanLogs" = "禁止日誌" "allClients" = "所有客戶" "addClient" = "新增客戶" "submitDisable" = "以停用方式送出 ☑️" "submitEnable" = "以啟用方式送出 ✅" "use_default" = "🏷️ 使用預設值" "change_id" = "⚙️🔑 ID" "change_password" = "⚙️🔑 密碼" "change_email" = "⚙️📧 電子郵件" "change_comment" = "⚙️💬 評論" "ResetAllTraffics" = "重設所有流量" "SortedTrafficUsageReport" = "排序過的流量使用報告" [tgbot.answers] "successfulOperation" = "✅ 成功!" "errorOperation" = "❗ 操作錯誤。" "getInboundsFailed" = "❌ 獲取入站資訊失敗。" "getClientsFailed" = "❌ 獲取客戶失敗。" "canceled" = "❌ {{ .Email }}:操作已取消。" "clientRefreshSuccess" = "✅ {{ .Email }}:客戶端重新整理成功。" "IpRefreshSuccess" = "✅ {{ .Email }}:IP 重新整理成功。" "TGIdRefreshSuccess" = "✅ {{ .Email }}:客戶端的 Telegram 使用者重新整理成功。" "resetTrafficSuccess" = "✅ {{ .Email }}:流量已重置成功。" "setTrafficLimitSuccess" = "✅ {{ .Email }}: 流量限制儲存成功。" "expireResetSuccess" = "✅ {{ .Email }}:過期天數已重置成功。" "resetIpSuccess" = "✅ {{ .Email }}:成功儲存 IP 限制數量為 {{ .Count }}。" "clearIpSuccess" = "✅ {{ .Email }}:IP 已成功清除。" "getIpLog" = "✅ {{ .Email }}:獲取 IP 日誌。" "getUserInfo" = "✅ {{ .Email }}:獲取 Telegram 使用者資訊。" "removedTGUserSuccess" = "✅ {{ .Email }}:Telegram 使用者已成功移除。" "enableSuccess" = "✅ {{ .Email }}:已成功啟用。" "disableSuccess" = "✅ {{ .Email }}:已成功禁用。" "askToAddUserId" = "未找到您的配置!\r\n請向管理員詢問,在您的配置中使用您的 Telegram 使用者 ChatID。\r\n\r\n您的使用者 ChatID:{{ .TgUserID }}" "chooseClient" = "為入站 {{ .Inbound }} 選擇一個客戶" "chooseInbound" = "選擇一個入站" ================================================ FILE: web/web.go ================================================ // Package web provides the main web server implementation for the 3x-ui panel, // including HTTP/HTTPS serving, routing, templates, and background job scheduling. package web import ( "context" "crypto/tls" "embed" "html/template" "io" "io/fs" "net" "net/http" "os" "strconv" "strings" "time" "github.com/mhsanaei/3x-ui/v2/config" "github.com/mhsanaei/3x-ui/v2/logger" "github.com/mhsanaei/3x-ui/v2/util/common" "github.com/mhsanaei/3x-ui/v2/web/controller" "github.com/mhsanaei/3x-ui/v2/web/job" "github.com/mhsanaei/3x-ui/v2/web/locale" "github.com/mhsanaei/3x-ui/v2/web/middleware" "github.com/mhsanaei/3x-ui/v2/web/network" "github.com/mhsanaei/3x-ui/v2/web/service" "github.com/mhsanaei/3x-ui/v2/web/websocket" "github.com/gin-contrib/gzip" "github.com/gin-contrib/sessions" "github.com/gin-contrib/sessions/cookie" "github.com/gin-gonic/gin" "github.com/robfig/cron/v3" ) //go:embed assets var assetsFS embed.FS //go:embed html/* var htmlFS embed.FS //go:embed translation/* var i18nFS embed.FS var startTime = time.Now() type wrapAssetsFS struct { embed.FS } func (f *wrapAssetsFS) Open(name string) (fs.File, error) { file, err := f.FS.Open("assets/" + name) if err != nil { return nil, err } return &wrapAssetsFile{ File: file, }, nil } type wrapAssetsFile struct { fs.File } func (f *wrapAssetsFile) Stat() (fs.FileInfo, error) { info, err := f.File.Stat() if err != nil { return nil, err } return &wrapAssetsFileInfo{ FileInfo: info, }, nil } type wrapAssetsFileInfo struct { fs.FileInfo } func (f *wrapAssetsFileInfo) ModTime() time.Time { return startTime } // EmbeddedHTML returns the embedded HTML templates filesystem for reuse by other servers. func EmbeddedHTML() embed.FS { return htmlFS } // EmbeddedAssets returns the embedded assets filesystem for reuse by other servers. func EmbeddedAssets() embed.FS { return assetsFS } // Server represents the main web server for the 3x-ui panel with controllers, services, and scheduled jobs. type Server struct { httpServer *http.Server listener net.Listener index *controller.IndexController panel *controller.XUIController api *controller.APIController ws *controller.WebSocketController xrayService service.XrayService settingService service.SettingService tgbotService service.Tgbot wsHub *websocket.Hub cron *cron.Cron ctx context.Context cancel context.CancelFunc } // NewServer creates a new web server instance with a cancellable context. func NewServer() *Server { ctx, cancel := context.WithCancel(context.Background()) return &Server{ ctx: ctx, cancel: cancel, } } // getHtmlFiles walks the local `web/html` directory and returns a list of // template file paths. Used only in debug/development mode. func (s *Server) getHtmlFiles() ([]string, error) { files := make([]string, 0) dir, _ := os.Getwd() err := fs.WalkDir(os.DirFS(dir), "web/html", func(path string, d fs.DirEntry, err error) error { if err != nil { return err } if d.IsDir() { return nil } files = append(files, path) return nil }) if err != nil { return nil, err } return files, nil } // getHtmlTemplate parses embedded HTML templates from the bundled `htmlFS` // using the provided template function map and returns the resulting // template set for production usage. func (s *Server) getHtmlTemplate(funcMap template.FuncMap) (*template.Template, error) { t := template.New("").Funcs(funcMap) err := fs.WalkDir(htmlFS, "html", func(path string, d fs.DirEntry, err error) error { if err != nil { return err } if d.IsDir() { newT, err := t.ParseFS(htmlFS, path+"/*.html") if err != nil { // ignore return nil } t = newT } return nil }) if err != nil { return nil, err } return t, nil } // initRouter initializes Gin, registers middleware, templates, static // assets, controllers and returns the configured engine. func (s *Server) initRouter() (*gin.Engine, error) { if config.IsDebug() { gin.SetMode(gin.DebugMode) } else { gin.DefaultWriter = io.Discard gin.DefaultErrorWriter = io.Discard gin.SetMode(gin.ReleaseMode) } engine := gin.Default() webDomain, err := s.settingService.GetWebDomain() if err != nil { return nil, err } if webDomain != "" { engine.Use(middleware.DomainValidatorMiddleware(webDomain)) } secret, err := s.settingService.GetSecret() if err != nil { return nil, err } basePath, err := s.settingService.GetBasePath() if err != nil { return nil, err } engine.Use(gzip.Gzip(gzip.DefaultCompression)) assetsBasePath := basePath + "assets/" store := cookie.NewStore(secret) // Configure default session cookie options, including expiration (MaxAge) if sessionMaxAge, err := s.settingService.GetSessionMaxAge(); err == nil { store.Options(sessions.Options{ Path: "/", MaxAge: sessionMaxAge * 60, // minutes -> seconds HttpOnly: true, SameSite: http.SameSiteLaxMode, }) } engine.Use(sessions.Sessions("3x-ui", store)) engine.Use(func(c *gin.Context) { c.Set("base_path", basePath) }) engine.Use(func(c *gin.Context) { uri := c.Request.RequestURI if strings.HasPrefix(uri, assetsBasePath) { c.Header("Cache-Control", "max-age=31536000") } }) // init i18n err = locale.InitLocalizer(i18nFS, &s.settingService) if err != nil { return nil, err } // Apply locale middleware for i18n i18nWebFunc := func(key string, params ...string) string { return locale.I18n(locale.Web, key, params...) } // Register template functions before loading templates funcMap := template.FuncMap{ "i18n": i18nWebFunc, } engine.SetFuncMap(funcMap) engine.Use(locale.LocalizerMiddleware()) // set static files and template if config.IsDebug() { // for development files, err := s.getHtmlFiles() if err != nil { return nil, err } // Use the registered func map with the loaded templates engine.LoadHTMLFiles(files...) engine.StaticFS(basePath+"assets", http.FS(os.DirFS("web/assets"))) } else { // for production template, err := s.getHtmlTemplate(funcMap) if err != nil { return nil, err } engine.SetHTMLTemplate(template) engine.StaticFS(basePath+"assets", http.FS(&wrapAssetsFS{FS: assetsFS})) } // Apply the redirect middleware (`/xui` to `/panel`) engine.Use(middleware.RedirectMiddleware(basePath)) g := engine.Group(basePath) s.index = controller.NewIndexController(g) s.panel = controller.NewXUIController(g) s.api = controller.NewAPIController(g) // Initialize WebSocket hub s.wsHub = websocket.NewHub() go s.wsHub.Run() // Initialize WebSocket controller s.ws = controller.NewWebSocketController(s.wsHub) // Register WebSocket route with basePath (g already has basePath prefix) g.GET("/ws", s.ws.HandleWebSocket) // Chrome DevTools endpoint for debugging web apps engine.GET("/.well-known/appspecific/com.chrome.devtools.json", func(c *gin.Context) { c.JSON(http.StatusOK, gin.H{}) }) // Add a catch-all route to handle undefined paths and return 404 engine.NoRoute(func(c *gin.Context) { c.AbortWithStatus(http.StatusNotFound) }) return engine, nil } // startTask schedules background jobs (Xray checks, traffic jobs, cron // jobs) which the panel relies on for periodic maintenance and monitoring. func (s *Server) startTask() { err := s.xrayService.RestartXray(true) if err != nil { logger.Warning("start xray failed:", err) } // Check whether xray is running every second s.cron.AddJob("@every 1s", job.NewCheckXrayRunningJob()) // Check if xray needs to be restarted every 30 seconds s.cron.AddFunc("@every 30s", func() { if s.xrayService.IsNeedRestartAndSetFalse() { err := s.xrayService.RestartXray(false) if err != nil { logger.Error("restart xray failed:", err) } } }) go func() { time.Sleep(time.Second * 5) // Statistics every 10 seconds, start the delay for 5 seconds for the first time, and staggered with the time to restart xray s.cron.AddJob("@every 10s", job.NewXrayTrafficJob()) }() // check client ips from log file every 10 sec s.cron.AddJob("@every 10s", job.NewCheckClientIpJob()) // check client ips from log file every day s.cron.AddJob("@daily", job.NewClearLogsJob()) // Inbound traffic reset jobs // Run once a day, midnight s.cron.AddJob("@daily", job.NewPeriodicTrafficResetJob("daily")) // Run once a week, midnight between Sat/Sun s.cron.AddJob("@weekly", job.NewPeriodicTrafficResetJob("weekly")) // Run once a month, midnight, first of month s.cron.AddJob("@monthly", job.NewPeriodicTrafficResetJob("monthly")) // LDAP sync scheduling if ldapEnabled, _ := s.settingService.GetLdapEnable(); ldapEnabled { runtime, err := s.settingService.GetLdapSyncCron() if err != nil || runtime == "" { runtime = "@every 1m" } j := job.NewLdapSyncJob() // job has zero-value services with method receivers that read settings on demand s.cron.AddJob(runtime, j) } // Make a traffic condition every day, 8:30 var entry cron.EntryID isTgbotenabled, err := s.settingService.GetTgbotEnabled() if (err == nil) && (isTgbotenabled) { runtime, err := s.settingService.GetTgbotRuntime() if err != nil || runtime == "" { logger.Errorf("Add NewStatsNotifyJob error[%s], Runtime[%s] invalid, will run default", err, runtime) runtime = "@daily" } logger.Infof("Tg notify enabled,run at %s", runtime) _, err = s.cron.AddJob(runtime, job.NewStatsNotifyJob()) if err != nil { logger.Warning("Add NewStatsNotifyJob error", err) return } // check for Telegram bot callback query hash storage reset s.cron.AddJob("@every 2m", job.NewCheckHashStorageJob()) // Check CPU load and alarm to TgBot if threshold passes cpuThreshold, err := s.settingService.GetTgCpu() if (err == nil) && (cpuThreshold > 0) { s.cron.AddJob("@every 10s", job.NewCheckCpuJob()) } } else { s.cron.Remove(entry) } } // Start initializes and starts the web server with configured settings, routes, and background jobs. func (s *Server) Start() (err error) { // This is an anonymous function, no function name defer func() { if err != nil { s.Stop() } }() loc, err := s.settingService.GetTimeLocation() if err != nil { return err } s.cron = cron.New(cron.WithLocation(loc), cron.WithSeconds()) s.cron.Start() engine, err := s.initRouter() if err != nil { return err } certFile, err := s.settingService.GetCertFile() if err != nil { return err } keyFile, err := s.settingService.GetKeyFile() if err != nil { return err } listen, err := s.settingService.GetListen() if err != nil { return err } port, err := s.settingService.GetPort() if err != nil { return err } listenAddr := net.JoinHostPort(listen, strconv.Itoa(port)) listener, err := net.Listen("tcp", listenAddr) if err != nil { return err } if certFile != "" || keyFile != "" { cert, err := tls.LoadX509KeyPair(certFile, keyFile) if err == nil { c := &tls.Config{ Certificates: []tls.Certificate{cert}, } listener = network.NewAutoHttpsListener(listener) listener = tls.NewListener(listener, c) logger.Info("Web server running HTTPS on", listener.Addr()) } else { logger.Error("Error loading certificates:", err) logger.Info("Web server running HTTP on", listener.Addr()) } } else { logger.Info("Web server running HTTP on", listener.Addr()) } s.listener = listener s.httpServer = &http.Server{ Handler: engine, } go func() { s.httpServer.Serve(listener) }() s.startTask() isTgbotenabled, err := s.settingService.GetTgbotEnabled() if (err == nil) && (isTgbotenabled) { tgBot := s.tgbotService.NewTgbot() tgBot.Start(i18nFS) } return nil } // Stop gracefully shuts down the web server, stops Xray, cron jobs, and Telegram bot. func (s *Server) Stop() error { s.cancel() s.xrayService.StopXray() if s.cron != nil { s.cron.Stop() } if s.tgbotService.IsRunning() { s.tgbotService.Stop() } // Gracefully stop WebSocket hub if s.wsHub != nil { s.wsHub.Stop() } var err1 error var err2 error if s.httpServer != nil { err1 = s.httpServer.Shutdown(s.ctx) } if s.listener != nil { err2 = s.listener.Close() } return common.Combine(err1, err2) } // GetCtx returns the server's context for cancellation and deadline management. func (s *Server) GetCtx() context.Context { return s.ctx } // GetCron returns the server's cron scheduler instance. func (s *Server) GetCron() *cron.Cron { return s.cron } // GetWSHub returns the WebSocket hub instance. func (s *Server) GetWSHub() any { return s.wsHub } func (s *Server) RestartXray() error { return s.xrayService.RestartXray(true) } ================================================ FILE: web/websocket/hub.go ================================================ // Package websocket provides WebSocket hub for real-time updates and notifications. package websocket import ( "context" "encoding/json" "runtime" "sync" "time" "github.com/mhsanaei/3x-ui/v2/logger" ) // MessageType represents the type of WebSocket message type MessageType string const ( MessageTypeStatus MessageType = "status" // Server status update MessageTypeTraffic MessageType = "traffic" // Traffic statistics update MessageTypeInbounds MessageType = "inbounds" // Inbounds list update MessageTypeNotification MessageType = "notification" // System notification MessageTypeXrayState MessageType = "xray_state" // Xray state change MessageTypeOutbounds MessageType = "outbounds" // Outbounds list update ) // Message represents a WebSocket message type Message struct { Type MessageType `json:"type"` Payload any `json:"payload"` Time int64 `json:"time"` } // Client represents a WebSocket client connection type Client struct { ID string Send chan []byte Hub *Hub Topics map[MessageType]bool // Subscribed topics } // Hub maintains the set of active clients and broadcasts messages to them type Hub struct { // Registered clients clients map[*Client]bool // Inbound messages from clients broadcast chan []byte // Register requests from clients register chan *Client // Unregister requests from clients unregister chan *Client // Mutex for thread-safe operations mu sync.RWMutex // Context for graceful shutdown ctx context.Context cancel context.CancelFunc // Worker pool for parallel broadcasting workerPoolSize int broadcastWg sync.WaitGroup } // NewHub creates a new WebSocket hub func NewHub() *Hub { ctx, cancel := context.WithCancel(context.Background()) // Calculate optimal worker pool size (CPU cores * 2, but max 100) workerPoolSize := runtime.NumCPU() * 2 if workerPoolSize > 100 { workerPoolSize = 100 } if workerPoolSize < 10 { workerPoolSize = 10 } return &Hub{ clients: make(map[*Client]bool), broadcast: make(chan []byte, 2048), // Increased from 256 to 2048 for high load register: make(chan *Client, 100), // Buffered channel for fast registration unregister: make(chan *Client, 100), // Buffered channel for fast unregistration ctx: ctx, cancel: cancel, workerPoolSize: workerPoolSize, } } // Run starts the hub's main loop func (h *Hub) Run() { defer func() { if r := recover(); r != nil { logger.Error("WebSocket hub panic recovered:", r) // Restart the hub loop go h.Run() } }() for { select { case <-h.ctx.Done(): // Graceful shutdown: close all clients h.mu.Lock() for client := range h.clients { // Safely close channel (avoid double close panic) select { case _, stillOpen := <-client.Send: if stillOpen { close(client.Send) } default: close(client.Send) } } h.clients = make(map[*Client]bool) h.mu.Unlock() // Wait for all broadcast workers to finish h.broadcastWg.Wait() logger.Info("WebSocket hub stopped gracefully") return case client := <-h.register: if client == nil { continue } h.mu.Lock() h.clients[client] = true count := len(h.clients) h.mu.Unlock() logger.Debugf("WebSocket client connected: %s (total: %d)", client.ID, count) case client := <-h.unregister: if client == nil { continue } h.mu.Lock() if _, ok := h.clients[client]; ok { delete(h.clients, client) // Safely close channel (avoid double close panic) // Check if channel is already closed by trying to read from it select { case _, stillOpen := <-client.Send: if stillOpen { // Channel was open and had data, now it's empty, safe to close close(client.Send) } // If stillOpen is false, channel was already closed, do nothing default: // Channel is empty and open, safe to close close(client.Send) } } count := len(h.clients) h.mu.Unlock() logger.Debugf("WebSocket client disconnected: %s (total: %d)", client.ID, count) case message := <-h.broadcast: if message == nil { continue } // Optimization: quickly copy client list and release lock h.mu.RLock() clientCount := len(h.clients) if clientCount == 0 { h.mu.RUnlock() continue } // Pre-allocate memory for client list clients := make([]*Client, 0, clientCount) for client := range h.clients { clients = append(clients, client) } h.mu.RUnlock() // Parallel broadcast using worker pool h.broadcastParallel(clients, message) } } } // broadcastParallel sends message to all clients in parallel for maximum performance func (h *Hub) broadcastParallel(clients []*Client, message []byte) { if len(clients) == 0 { return } // For small number of clients, use simple parallel sending if len(clients) < h.workerPoolSize { var wg sync.WaitGroup for _, client := range clients { wg.Add(1) go func(c *Client) { defer wg.Done() defer func() { if r := recover(); r != nil { // Channel may be closed, safely ignore logger.Debugf("WebSocket broadcast panic recovered for client %s: %v", c.ID, r) } }() select { case c.Send <- message: default: // Client's send buffer is full, disconnect logger.Debugf("WebSocket client %s send buffer full, disconnecting", c.ID) h.Unregister(c) } }(client) } wg.Wait() return } // For large number of clients, use worker pool for optimal performance clientChan := make(chan *Client, len(clients)) for _, client := range clients { clientChan <- client } close(clientChan) // Start workers for parallel processing h.broadcastWg.Add(h.workerPoolSize) for i := 0; i < h.workerPoolSize; i++ { go func() { defer h.broadcastWg.Done() for client := range clientChan { func() { defer func() { if r := recover(); r != nil { // Channel may be closed, safely ignore logger.Debugf("WebSocket broadcast panic recovered for client %s: %v", client.ID, r) } }() select { case client.Send <- message: default: // Client's send buffer is full, disconnect logger.Debugf("WebSocket client %s send buffer full, disconnecting", client.ID) h.Unregister(client) } }() } }() } // Wait for all workers to finish h.broadcastWg.Wait() } // Broadcast sends a message to all connected clients func (h *Hub) Broadcast(messageType MessageType, payload any) { if h == nil { return } if payload == nil { logger.Warning("Attempted to broadcast nil payload") return } msg := Message{ Type: messageType, Payload: payload, Time: getCurrentTimestamp(), } data, err := json.Marshal(msg) if err != nil { logger.Error("Failed to marshal WebSocket message:", err) return } // Limit message size to prevent memory issues const maxMessageSize = 1024 * 1024 // 1MB if len(data) > maxMessageSize { logger.Warningf("WebSocket message too large: %d bytes, dropping", len(data)) return } // Non-blocking send with timeout to prevent delays select { case h.broadcast <- data: case <-time.After(100 * time.Millisecond): logger.Warning("WebSocket broadcast channel is full, dropping message") case <-h.ctx.Done(): // Hub is shutting down } } // BroadcastToTopic sends a message only to clients subscribed to the specific topic func (h *Hub) BroadcastToTopic(messageType MessageType, payload any) { if h == nil { return } if payload == nil { logger.Warning("Attempted to broadcast nil payload to topic") return } msg := Message{ Type: messageType, Payload: payload, Time: getCurrentTimestamp(), } data, err := json.Marshal(msg) if err != nil { logger.Error("Failed to marshal WebSocket message:", err) return } // Limit message size to prevent memory issues const maxMessageSize = 1024 * 1024 // 1MB if len(data) > maxMessageSize { logger.Warningf("WebSocket message too large: %d bytes, dropping", len(data)) return } h.mu.RLock() // Filter clients by topics and quickly release lock subscribedClients := make([]*Client, 0) for client := range h.clients { if len(client.Topics) == 0 || client.Topics[messageType] { subscribedClients = append(subscribedClients, client) } } h.mu.RUnlock() // Parallel send to subscribed clients if len(subscribedClients) > 0 { h.broadcastParallel(subscribedClients, data) } } // GetClientCount returns the number of connected clients func (h *Hub) GetClientCount() int { h.mu.RLock() defer h.mu.RUnlock() return len(h.clients) } // Register registers a new client with the hub func (h *Hub) Register(client *Client) { if h == nil || client == nil { return } select { case h.register <- client: case <-h.ctx.Done(): // Hub is shutting down } } // Unregister unregisters a client from the hub func (h *Hub) Unregister(client *Client) { if h == nil || client == nil { return } select { case h.unregister <- client: case <-h.ctx.Done(): // Hub is shutting down } } // Stop gracefully stops the hub and closes all connections func (h *Hub) Stop() { if h == nil { return } if h.cancel != nil { h.cancel() } } // getCurrentTimestamp returns current Unix timestamp in milliseconds func getCurrentTimestamp() int64 { return time.Now().UnixMilli() } ================================================ FILE: web/websocket/notifier.go ================================================ // Package websocket provides WebSocket hub for real-time updates and notifications. package websocket import ( "github.com/mhsanaei/3x-ui/v2/logger" "github.com/mhsanaei/3x-ui/v2/web/global" ) // GetHub returns the global WebSocket hub instance func GetHub() *Hub { webServer := global.GetWebServer() if webServer == nil { return nil } hub := webServer.GetWSHub() if hub == nil { return nil } wsHub, ok := hub.(*Hub) if !ok { logger.Warning("WebSocket hub type assertion failed") return nil } return wsHub } // BroadcastStatus broadcasts server status update to all connected clients func BroadcastStatus(status any) { hub := GetHub() if hub != nil { hub.Broadcast(MessageTypeStatus, status) } } // BroadcastTraffic broadcasts traffic statistics update to all connected clients func BroadcastTraffic(traffic any) { hub := GetHub() if hub != nil { hub.Broadcast(MessageTypeTraffic, traffic) } } // BroadcastInbounds broadcasts inbounds list update to all connected clients func BroadcastInbounds(inbounds any) { hub := GetHub() if hub != nil { hub.Broadcast(MessageTypeInbounds, inbounds) } } // BroadcastOutbounds broadcasts outbounds list update to all connected clients func BroadcastOutbounds(outbounds any) { hub := GetHub() if hub != nil { hub.Broadcast(MessageTypeOutbounds, outbounds) } } // BroadcastNotification broadcasts a system notification to all connected clients func BroadcastNotification(title, message, level string) { hub := GetHub() if hub != nil { notification := map[string]string{ "title": title, "message": message, "level": level, // info, warning, error, success } hub.Broadcast(MessageTypeNotification, notification) } } // BroadcastXrayState broadcasts Xray state change to all connected clients func BroadcastXrayState(state string, errorMsg string) { hub := GetHub() if hub != nil { stateUpdate := map[string]string{ "state": state, "errorMsg": errorMsg, } hub.Broadcast(MessageTypeXrayState, stateUpdate) } } ================================================ FILE: windows_files/readme.txt ================================================ you can't install fail2ban on windows we don't have bash menu for windows if you forgot your password you need to check your database with https://sqlitebrowser.org/ the app need to be open all the time default setting: http://localhost:2053/ user: admin pass: admin port: 2053 openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout localhost.key -out localhost.crt ================================================ FILE: x-ui.rc ================================================ #!/sbin/openrc-run command="/usr/local/x-ui/x-ui" command_background=true pidfile="/run/x-ui.pid" description="x-ui Service" procname="x-ui" depend() { need net } start_pre(){ cd /usr/local/x-ui } reload() { ebegin "Reloading ${RC_SVCNAME}" kill -USR1 $pidfile eend $? } ================================================ FILE: x-ui.service.arch ================================================ [Unit] Description=x-ui Service After=network.target Wants=network.target [Service] EnvironmentFile=-/etc/conf.d/x-ui Environment="XRAY_VMESS_AEAD_FORCED=false" Type=simple WorkingDirectory=/usr/lib/x-ui/ ExecStart=/usr/lib/x-ui/x-ui ExecReload=kill -USR1 $MAINPID Restart=on-failure RestartSec=5s [Install] WantedBy=multi-user.target ================================================ FILE: x-ui.service.debian ================================================ [Unit] Description=x-ui Service After=network.target Wants=network.target [Service] EnvironmentFile=-/etc/default/x-ui Environment="XRAY_VMESS_AEAD_FORCED=false" Type=simple WorkingDirectory=/usr/local/x-ui/ ExecStart=/usr/local/x-ui/x-ui ExecReload=kill -USR1 $MAINPID Restart=on-failure RestartSec=5s [Install] WantedBy=multi-user.target ================================================ FILE: x-ui.service.rhel ================================================ [Unit] Description=x-ui Service After=network.target Wants=network.target [Service] EnvironmentFile=-/etc/sysconfig/x-ui Environment="XRAY_VMESS_AEAD_FORCED=false" Type=simple WorkingDirectory=/usr/local/x-ui/ ExecStart=/usr/local/x-ui/x-ui ExecReload=kill -USR1 $MAINPID Restart=on-failure RestartSec=5s [Install] WantedBy=multi-user.target ================================================ FILE: x-ui.sh ================================================ #!/bin/bash red='\033[0;31m' green='\033[0;32m' blue='\033[0;34m' yellow='\033[0;33m' plain='\033[0m' #Add some basic function here function LOGD() { echo -e "${yellow}[DEG] $* ${plain}" } function LOGE() { echo -e "${red}[ERR] $* ${plain}" } function LOGI() { echo -e "${green}[INF] $* ${plain}" } # Port helpers: detect listener and owning process (best effort) is_port_in_use() { local port="$1" if command -v ss >/dev/null 2>&1; then ss -ltn 2>/dev/null | awk -v p=":${port}$" '$4 ~ p {exit 0} END {exit 1}' return fi if command -v netstat >/dev/null 2>&1; then netstat -lnt 2>/dev/null | awk -v p=":${port} " '$4 ~ p {exit 0} END {exit 1}' return fi if command -v lsof >/dev/null 2>&1; then lsof -nP -iTCP:${port} -sTCP:LISTEN >/dev/null 2>&1 && return 0 fi return 1 } # Simple helpers for domain/IP validation is_ipv4() { [[ "$1" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]] && return 0 || return 1 } is_ipv6() { [[ "$1" =~ : ]] && return 0 || return 1 } is_ip() { is_ipv4 "$1" || is_ipv6 "$1" } is_domain() { [[ "$1" =~ ^([A-Za-z0-9](-*[A-Za-z0-9])*\.)+(xn--[a-z0-9]{2,}|[A-Za-z]{2,})$ ]] && return 0 || return 1 } # check root [[ $EUID -ne 0 ]] && LOGE "ERROR: You must be root to run this script! \n" && exit 1 # Check OS and set release variable if [[ -f /etc/os-release ]]; then source /etc/os-release release=$ID elif [[ -f /usr/lib/os-release ]]; then source /usr/lib/os-release release=$ID else echo "Failed to check the system OS, please contact the author!" >&2 exit 1 fi echo "The OS release is: $release" os_version="" os_version=$(grep "^VERSION_ID" /etc/os-release | cut -d '=' -f2 | tr -d '"' | tr -d '.') # Declare Variables xui_folder="${XUI_MAIN_FOLDER:=/usr/local/x-ui}" xui_service="${XUI_SERVICE:=/etc/systemd/system}" log_folder="${XUI_LOG_FOLDER:=/var/log/x-ui}" mkdir -p "${log_folder}" iplimit_log_path="${log_folder}/3xipl.log" iplimit_banned_log_path="${log_folder}/3xipl-banned.log" confirm() { if [[ $# > 1 ]]; then echo && read -rp "$1 [Default $2]: " temp if [[ "${temp}" == "" ]]; then temp=$2 fi else read -rp "$1 [y/n]: " temp fi if [[ "${temp}" == "y" || "${temp}" == "Y" ]]; then return 0 else return 1 fi } confirm_restart() { confirm "Restart the panel, Attention: Restarting the panel will also restart xray" "y" if [[ $? == 0 ]]; then restart else show_menu fi } before_show_menu() { echo && echo -n -e "${yellow}Press enter to return to the main menu: ${plain}" && read -r temp show_menu } install() { bash <(curl -Ls https://raw.githubusercontent.com/MHSanaei/3x-ui/main/install.sh) if [[ $? == 0 ]]; then if [[ $# == 0 ]]; then start else start 0 fi fi } update() { confirm "This function will update all x-ui components to the latest version, and the data will not be lost. Do you want to continue?" "y" if [[ $? != 0 ]]; then LOGE "Cancelled" if [[ $# == 0 ]]; then before_show_menu fi return 0 fi bash <(curl -Ls https://raw.githubusercontent.com/MHSanaei/3x-ui/main/update.sh) if [[ $? == 0 ]]; then LOGI "Update is complete, Panel has automatically restarted " before_show_menu fi } update_menu() { echo -e "${yellow}Updating Menu${plain}" confirm "This function will update the menu to the latest changes." "y" if [[ $? != 0 ]]; then LOGE "Cancelled" if [[ $# == 0 ]]; then before_show_menu fi return 0 fi curl -fLRo /usr/bin/x-ui https://raw.githubusercontent.com/MHSanaei/3x-ui/main/x-ui.sh chmod +x ${xui_folder}/x-ui.sh chmod +x /usr/bin/x-ui if [[ $? == 0 ]]; then echo -e "${green}Update successful. The panel has automatically restarted.${plain}" exit 0 else echo -e "${red}Failed to update the menu.${plain}" return 1 fi } legacy_version() { echo -n "Enter the panel version (like 2.4.0):" read -r tag_version if [ -z "$tag_version" ]; then echo "Panel version cannot be empty. Exiting." exit 1 fi # Use the entered panel version in the download link install_command="bash <(curl -Ls "https://raw.githubusercontent.com/mhsanaei/3x-ui/v$tag_version/install.sh") v$tag_version" echo "Downloading and installing panel version $tag_version..." eval $install_command } # Function to handle the deletion of the script file delete_script() { rm "$0" # Remove the script file itself exit 1 } uninstall() { confirm "Are you sure you want to uninstall the panel? xray will also uninstalled!" "n" if [[ $? != 0 ]]; then if [[ $# == 0 ]]; then show_menu fi return 0 fi if [[ $release == "alpine" ]]; then rc-service x-ui stop rc-update del x-ui rm /etc/init.d/x-ui -f else systemctl stop x-ui systemctl disable x-ui rm ${xui_service}/x-ui.service -f systemctl daemon-reload systemctl reset-failed fi rm /etc/x-ui/ -rf rm ${xui_folder}/ -rf echo "" echo -e "Uninstalled Successfully.\n" echo "If you need to install this panel again, you can use below command:" echo -e "${green}bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.sh)${plain}" echo "" # Trap the SIGTERM signal trap delete_script SIGTERM delete_script } reset_user() { confirm "Are you sure to reset the username and password of the panel?" "n" if [[ $? != 0 ]]; then if [[ $# == 0 ]]; then show_menu fi return 0 fi read -rp "Please set the login username [default is a random username]: " config_account [[ -z $config_account ]] && config_account=$(gen_random_string 10) read -rp "Please set the login password [default is a random password]: " config_password [[ -z $config_password ]] && config_password=$(gen_random_string 18) read -rp "Do you want to disable currently configured two-factor authentication? (y/n): " twoFactorConfirm if [[ $twoFactorConfirm != "y" && $twoFactorConfirm != "Y" ]]; then ${xui_folder}/x-ui setting -username "${config_account}" -password "${config_password}" -resetTwoFactor false >/dev/null 2>&1 else ${xui_folder}/x-ui setting -username "${config_account}" -password "${config_password}" -resetTwoFactor true >/dev/null 2>&1 echo -e "Two factor authentication has been disabled." fi echo -e "Panel login username has been reset to: ${green} ${config_account} ${plain}" echo -e "Panel login password has been reset to: ${green} ${config_password} ${plain}" echo -e "${green} Please use the new login username and password to access the X-UI panel. Also remember them! ${plain}" confirm_restart } gen_random_string() { local length="$1" local random_string=$(LC_ALL=C tr -dc 'a-zA-Z0-9' /dev/null 2>&1 echo -e "Web base path has been reset to: ${green}${config_webBasePath}${plain}" echo -e "${green}Please use the new web base path to access the panel.${plain}" restart } reset_config() { confirm "Are you sure you want to reset all panel settings, Account data will not be lost, Username and password will not change" "n" if [[ $? != 0 ]]; then if [[ $# == 0 ]]; then show_menu fi return 0 fi ${xui_folder}/x-ui setting -reset echo -e "All panel settings have been reset to default." restart } check_config() { local info=$(${xui_folder}/x-ui setting -show true) if [[ $? != 0 ]]; then LOGE "get current settings error, please check logs" show_menu return fi LOGI "${info}" local existing_webBasePath=$(echo "$info" | grep -Eo 'webBasePath: .+' | awk '{print $2}') local existing_port=$(echo "$info" | grep -Eo 'port: .+' | awk '{print $2}') local existing_cert=$(${xui_folder}/x-ui setting -getCert true | grep 'cert:' | awk -F': ' '{print $2}' | tr -d '[:space:]') local server_ip=$(curl -s --max-time 3 https://api.ipify.org) if [ -z "$server_ip" ]; then server_ip=$(curl -s --max-time 3 https://4.ident.me) fi if [[ -n "$existing_cert" ]]; then local domain=$(basename "$(dirname "$existing_cert")") if [[ "$domain" =~ ^[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$ ]]; then echo -e "${green}Access URL: https://${domain}:${existing_port}${existing_webBasePath}${plain}" else echo -e "${green}Access URL: https://${server_ip}:${existing_port}${existing_webBasePath}${plain}" fi else echo -e "${red}⚠ WARNING: No SSL certificate configured!${plain}" echo -e "${yellow}You can get a Let's Encrypt certificate for your IP address (valid ~6 days, auto-renews).${plain}" read -rp "Generate SSL certificate for IP now? [y/N]: " gen_ssl if [[ "$gen_ssl" == "y" || "$gen_ssl" == "Y" ]]; then stop >/dev/null 2>&1 ssl_cert_issue_for_ip if [[ $? -eq 0 ]]; then echo -e "${green}Access URL: https://${server_ip}:${existing_port}${existing_webBasePath}${plain}" # ssl_cert_issue_for_ip already restarts the panel, but ensure it's running start >/dev/null 2>&1 else LOGE "IP certificate setup failed." echo -e "${yellow}You can try again via option 19 (SSL Certificate Management).${plain}" start >/dev/null 2>&1 fi else echo -e "${yellow}Access URL: http://${server_ip}:${existing_port}${existing_webBasePath}${plain}" echo -e "${yellow}For security, please configure SSL certificate using option 19 (SSL Certificate Management)${plain}" fi fi } set_port() { echo -n "Enter port number[1-65535]: " read -r port if [[ -z "${port}" ]]; then LOGD "Cancelled" before_show_menu else ${xui_folder}/x-ui setting -port ${port} echo -e "The port is set, Please restart the panel now, and use the new port ${green}${port}${plain} to access web panel" confirm_restart fi } start() { check_status if [[ $? == 0 ]]; then echo "" LOGI "Panel is running, No need to start again, If you need to restart, please select restart" else if [[ $release == "alpine" ]]; then rc-service x-ui start else systemctl start x-ui fi sleep 2 check_status if [[ $? == 0 ]]; then LOGI "x-ui Started Successfully" else LOGE "panel Failed to start, Probably because it takes longer than two seconds to start, Please check the log information later" fi fi if [[ $# == 0 ]]; then before_show_menu fi } stop() { check_status if [[ $? == 1 ]]; then echo "" LOGI "Panel stopped, No need to stop again!" else if [[ $release == "alpine" ]]; then rc-service x-ui stop else systemctl stop x-ui fi sleep 2 check_status if [[ $? == 1 ]]; then LOGI "x-ui and xray stopped successfully" else LOGE "Panel stop failed, Probably because the stop time exceeds two seconds, Please check the log information later" fi fi if [[ $# == 0 ]]; then before_show_menu fi } restart() { if [[ $release == "alpine" ]]; then rc-service x-ui restart else systemctl restart x-ui fi sleep 2 check_status if [[ $? == 0 ]]; then LOGI "x-ui and xray Restarted successfully" else LOGE "Panel restart failed, Probably because it takes longer than two seconds to start, Please check the log information later" fi if [[ $# == 0 ]]; then before_show_menu fi } restart_xray() { systemctl reload x-ui LOGI "xray-core Restart signal sent successfully, Please check the log information to confirm whether xray restarted successfully" sleep 2 show_xray_status if [[ $# == 0 ]]; then before_show_menu fi } status() { if [[ $release == "alpine" ]]; then rc-service x-ui status else systemctl status x-ui -l fi if [[ $# == 0 ]]; then before_show_menu fi } enable() { if [[ $release == "alpine" ]]; then rc-update add x-ui default else systemctl enable x-ui fi if [[ $? == 0 ]]; then LOGI "x-ui Set to boot automatically on startup successfully" else LOGE "x-ui Failed to set Autostart" fi if [[ $# == 0 ]]; then before_show_menu fi } disable() { if [[ $release == "alpine" ]]; then rc-update del x-ui else systemctl disable x-ui fi if [[ $? == 0 ]]; then LOGI "x-ui Autostart Cancelled successfully" else LOGE "x-ui Failed to cancel autostart" fi if [[ $# == 0 ]]; then before_show_menu fi } show_log() { if [[ $release == "alpine" ]]; then echo -e "${green}\t1.${plain} Debug Log" echo -e "${green}\t0.${plain} Back to Main Menu" read -rp "Choose an option: " choice case "$choice" in 0) show_menu ;; 1) grep -F 'x-ui[' /var/log/messages if [[ $# == 0 ]]; then before_show_menu fi ;; *) echo -e "${red}Invalid option. Please select a valid number.${plain}\n" show_log ;; esac else echo -e "${green}\t1.${plain} Debug Log" echo -e "${green}\t2.${plain} Clear All logs" echo -e "${green}\t0.${plain} Back to Main Menu" read -rp "Choose an option: " choice case "$choice" in 0) show_menu ;; 1) journalctl -u x-ui -e --no-pager -f -p debug if [[ $# == 0 ]]; then before_show_menu fi ;; 2) sudo journalctl --rotate sudo journalctl --vacuum-time=1s echo "All Logs cleared." restart ;; *) echo -e "${red}Invalid option. Please select a valid number.${plain}\n" show_log ;; esac fi } bbr_menu() { echo -e "${green}\t1.${plain} Enable BBR" echo -e "${green}\t2.${plain} Disable BBR" echo -e "${green}\t0.${plain} Back to Main Menu" read -rp "Choose an option: " choice case "$choice" in 0) show_menu ;; 1) enable_bbr bbr_menu ;; 2) disable_bbr bbr_menu ;; *) echo -e "${red}Invalid option. Please select a valid number.${plain}\n" bbr_menu ;; esac } disable_bbr() { if [[ $(sysctl -n net.ipv4.tcp_congestion_control) != "bbr" ]] || [[ ! $(sysctl -n net.core.default_qdisc) =~ ^(fq|cake)$ ]]; then echo -e "${yellow}BBR is not currently enabled.${plain}" before_show_menu fi if [ -f "/etc/sysctl.d/99-bbr-x-ui.conf" ]; then old_settings=$(head -1 /etc/sysctl.d/99-bbr-x-ui.conf | tr -d '#') sysctl -w net.core.default_qdisc="${old_settings%:*}" sysctl -w net.ipv4.tcp_congestion_control="${old_settings#*:}" rm /etc/sysctl.d/99-bbr-x-ui.conf sysctl --system else # Replace BBR with CUBIC configurations if [ -f "/etc/sysctl.conf" ]; then sed -i 's/net.core.default_qdisc=fq/net.core.default_qdisc=pfifo_fast/' /etc/sysctl.conf sed -i 's/net.ipv4.tcp_congestion_control=bbr/net.ipv4.tcp_congestion_control=cubic/' /etc/sysctl.conf sysctl -p fi fi if [[ $(sysctl -n net.ipv4.tcp_congestion_control) != "bbr" ]]; then echo -e "${green}BBR has been replaced with CUBIC successfully.${plain}" else echo -e "${red}Failed to replace BBR with CUBIC. Please check your system configuration.${plain}" fi } enable_bbr() { if [[ $(sysctl -n net.ipv4.tcp_congestion_control) == "bbr" ]] && [[ $(sysctl -n net.core.default_qdisc) =~ ^(fq|cake)$ ]]; then echo -e "${green}BBR is already enabled!${plain}" before_show_menu fi # Enable BBR if [ -d "/etc/sysctl.d/" ]; then { echo "#$(sysctl -n net.core.default_qdisc):$(sysctl -n net.ipv4.tcp_congestion_control)" echo "net.core.default_qdisc = fq" echo "net.ipv4.tcp_congestion_control = bbr" } > "/etc/sysctl.d/99-bbr-x-ui.conf" if [ -f "/etc/sysctl.conf" ]; then # Backup old settings from sysctl.conf, if any sed -i 's/^net.core.default_qdisc/# &/' /etc/sysctl.conf sed -i 's/^net.ipv4.tcp_congestion_control/# &/' /etc/sysctl.conf fi sysctl --system else sed -i '/net.core.default_qdisc/d' /etc/sysctl.conf sed -i '/net.ipv4.tcp_congestion_control/d' /etc/sysctl.conf echo "net.core.default_qdisc=fq" | tee -a /etc/sysctl.conf echo "net.ipv4.tcp_congestion_control=bbr" | tee -a /etc/sysctl.conf sysctl -p fi # Verify that BBR is enabled if [[ $(sysctl -n net.ipv4.tcp_congestion_control) == "bbr" ]]; then echo -e "${green}BBR has been enabled successfully.${plain}" else echo -e "${red}Failed to enable BBR. Please check your system configuration.${plain}" fi } update_shell() { curl -fLRo /usr/bin/x-ui -z /usr/bin/x-ui https://github.com/MHSanaei/3x-ui/raw/main/x-ui.sh if [[ $? != 0 ]]; then echo "" LOGE "Failed to download script, Please check whether the machine can connect Github" before_show_menu else chmod +x /usr/bin/x-ui LOGI "Upgrade script succeeded, Please rerun the script" before_show_menu fi } # 0: running, 1: not running, 2: not installed check_status() { if [[ $release == "alpine" ]]; then if [[ ! -f /etc/init.d/x-ui ]]; then return 2 fi if [[ $(rc-service x-ui status | grep -F 'status: started' -c) == 1 ]]; then return 0 else return 1 fi else if [[ ! -f ${xui_service}/x-ui.service ]]; then return 2 fi temp=$(systemctl status x-ui | grep Active | awk '{print $3}' | cut -d "(" -f2 | cut -d ")" -f1) if [[ "${temp}" == "running" ]]; then return 0 else return 1 fi fi } check_enabled() { if [[ $release == "alpine" ]]; then if [[ $(rc-update show | grep -F 'x-ui' | grep default -c) == 1 ]]; then return 0 else return 1 fi else temp=$(systemctl is-enabled x-ui) if [[ "${temp}" == "enabled" ]]; then return 0 else return 1 fi fi } check_uninstall() { check_status if [[ $? != 2 ]]; then echo "" LOGE "Panel installed, Please do not reinstall" if [[ $# == 0 ]]; then before_show_menu fi return 1 else return 0 fi } check_install() { check_status if [[ $? == 2 ]]; then echo "" LOGE "Please install the panel first" if [[ $# == 0 ]]; then before_show_menu fi return 1 else return 0 fi } show_status() { check_status case $? in 0) echo -e "Panel state: ${green}Running${plain}" show_enable_status ;; 1) echo -e "Panel state: ${yellow}Not Running${plain}" show_enable_status ;; 2) echo -e "Panel state: ${red}Not Installed${plain}" ;; esac show_xray_status } show_enable_status() { check_enabled if [[ $? == 0 ]]; then echo -e "Start automatically: ${green}Yes${plain}" else echo -e "Start automatically: ${red}No${plain}" fi } check_xray_status() { count=$(ps -ef | grep "xray-linux" | grep -v "grep" | wc -l) if [[ count -ne 0 ]]; then return 0 else return 1 fi } show_xray_status() { check_xray_status if [[ $? == 0 ]]; then echo -e "xray state: ${green}Running${plain}" else echo -e "xray state: ${red}Not Running${plain}" fi } firewall_menu() { echo -e "${green}\t1.${plain} ${green}Install${plain} Firewall" echo -e "${green}\t2.${plain} Port List [numbered]" echo -e "${green}\t3.${plain} ${green}Open${plain} Ports" echo -e "${green}\t4.${plain} ${red}Delete${plain} Ports from List" echo -e "${green}\t5.${plain} ${green}Enable${plain} Firewall" echo -e "${green}\t6.${plain} ${red}Disable${plain} Firewall" echo -e "${green}\t7.${plain} Firewall Status" echo -e "${green}\t0.${plain} Back to Main Menu" read -rp "Choose an option: " choice case "$choice" in 0) show_menu ;; 1) install_firewall firewall_menu ;; 2) ufw status numbered firewall_menu ;; 3) open_ports firewall_menu ;; 4) delete_ports firewall_menu ;; 5) ufw enable firewall_menu ;; 6) ufw disable firewall_menu ;; 7) ufw status verbose firewall_menu ;; *) echo -e "${red}Invalid option. Please select a valid number.${plain}\n" firewall_menu ;; esac } install_firewall() { if ! command -v ufw &>/dev/null; then echo "ufw firewall is not installed. Installing now..." apt-get update apt-get install -y ufw else echo "ufw firewall is already installed" fi # Check if the firewall is inactive if ufw status | grep -q "Status: active"; then echo "Firewall is already active" else echo "Activating firewall..." # Open the necessary ports ufw allow ssh ufw allow http ufw allow https ufw allow 2053/tcp #webPort ufw allow 2096/tcp #subport # Enable the firewall ufw --force enable fi } open_ports() { # Prompt the user to enter the ports they want to open read -rp "Enter the ports you want to open (e.g. 80,443,2053 or range 400-500): " ports # Check if the input is valid if ! [[ $ports =~ ^([0-9]+|[0-9]+-[0-9]+)(,([0-9]+|[0-9]+-[0-9]+))*$ ]]; then echo "Error: Invalid input. Please enter a comma-separated list of ports or a range of ports (e.g. 80,443,2053 or 400-500)." >&2 exit 1 fi # Open the specified ports using ufw IFS=',' read -ra PORT_LIST <<<"$ports" for port in "${PORT_LIST[@]}"; do if [[ $port == *-* ]]; then # Split the range into start and end ports start_port=$(echo $port | cut -d'-' -f1) end_port=$(echo $port | cut -d'-' -f2) # Open the port range ufw allow $start_port:$end_port/tcp ufw allow $start_port:$end_port/udp else # Open the single port ufw allow "$port" fi done # Confirm that the ports are opened echo "Opened the specified ports:" for port in "${PORT_LIST[@]}"; do if [[ $port == *-* ]]; then start_port=$(echo $port | cut -d'-' -f1) end_port=$(echo $port | cut -d'-' -f2) # Check if the port range has been successfully opened (ufw status | grep -q "$start_port:$end_port") && echo "$start_port-$end_port" else # Check if the individual port has been successfully opened (ufw status | grep -q "$port") && echo "$port" fi done } delete_ports() { # Display current rules with numbers echo "Current UFW rules:" ufw status numbered # Ask the user how they want to delete rules echo "Do you want to delete rules by:" echo "1) Rule numbers" echo "2) Ports" read -rp "Enter your choice (1 or 2): " choice if [[ $choice -eq 1 ]]; then # Deleting by rule numbers read -rp "Enter the rule numbers you want to delete (1, 2, etc.): " rule_numbers # Validate the input if ! [[ $rule_numbers =~ ^([0-9]+)(,[0-9]+)*$ ]]; then echo "Error: Invalid input. Please enter a comma-separated list of rule numbers." >&2 exit 1 fi # Split numbers into an array IFS=',' read -ra RULE_NUMBERS <<<"$rule_numbers" for rule_number in "${RULE_NUMBERS[@]}"; do # Delete the rule by number ufw delete "$rule_number" || echo "Failed to delete rule number $rule_number" done echo "Selected rules have been deleted." elif [[ $choice -eq 2 ]]; then # Deleting by ports read -rp "Enter the ports you want to delete (e.g. 80,443,2053 or range 400-500): " ports # Validate the input if ! [[ $ports =~ ^([0-9]+|[0-9]+-[0-9]+)(,([0-9]+|[0-9]+-[0-9]+))*$ ]]; then echo "Error: Invalid input. Please enter a comma-separated list of ports or a range of ports (e.g. 80,443,2053 or 400-500)." >&2 exit 1 fi # Split ports into an array IFS=',' read -ra PORT_LIST <<<"$ports" for port in "${PORT_LIST[@]}"; do if [[ $port == *-* ]]; then # Split the port range start_port=$(echo $port | cut -d'-' -f1) end_port=$(echo $port | cut -d'-' -f2) # Delete the port range ufw delete allow $start_port:$end_port/tcp ufw delete allow $start_port:$end_port/udp else # Delete a single port ufw delete allow "$port" fi done # Confirmation of deletion echo "Deleted the specified ports:" for port in "${PORT_LIST[@]}"; do if [[ $port == *-* ]]; then start_port=$(echo $port | cut -d'-' -f1) end_port=$(echo $port | cut -d'-' -f2) # Check if the port range has been deleted (ufw status | grep -q "$start_port:$end_port") || echo "$start_port-$end_port" else # Check if the individual port has been deleted (ufw status | grep -q "$port") || echo "$port" fi done else echo "${red}Error:${plain} Invalid choice. Please enter 1 or 2." >&2 exit 1 fi } update_all_geofiles() { update_geofiles "main" update_geofiles "IR" update_geofiles "RU" } update_geofiles() { case "${1}" in "main") dat_files=(geoip geosite); dat_source="Loyalsoldier/v2ray-rules-dat";; "IR") dat_files=(geoip_IR geosite_IR); dat_source="chocolate4u/Iran-v2ray-rules" ;; "RU") dat_files=(geoip_RU geosite_RU); dat_source="runetfreedom/russia-v2ray-rules-dat";; esac for dat in "${dat_files[@]}"; do # Remove suffix for remote filename (e.g., geoip_IR -> geoip) remote_file="${dat%%_*}" curl -fLRo ${xui_folder}/bin/${dat}.dat -z ${xui_folder}/bin/${dat}.dat \ https://github.com/${dat_source}/releases/latest/download/${remote_file}.dat done } update_geo() { echo -e "${green}\t1.${plain} Loyalsoldier (geoip.dat, geosite.dat)" echo -e "${green}\t2.${plain} chocolate4u (geoip_IR.dat, geosite_IR.dat)" echo -e "${green}\t3.${plain} runetfreedom (geoip_RU.dat, geosite_RU.dat)" echo -e "${green}\t4.${plain} All" echo -e "${green}\t0.${plain} Back to Main Menu" read -rp "Choose an option: " choice case "$choice" in 0) show_menu ;; 1) update_geofiles "main" echo -e "${green}Loyalsoldier datasets have been updated successfully!${plain}" restart ;; 2) update_geofiles "IR" echo -e "${green}chocolate4u datasets have been updated successfully!${plain}" restart ;; 3) update_geofiles "RU" echo -e "${green}runetfreedom datasets have been updated successfully!${plain}" restart ;; 4) update_all_geofiles echo -e "${green}All geo files have been updated successfully!${plain}" restart ;; *) echo -e "${red}Invalid option. Please select a valid number.${plain}\n" update_geo ;; esac before_show_menu } install_acme() { # Check if acme.sh is already installed if command -v ~/.acme.sh/acme.sh &>/dev/null; then LOGI "acme.sh is already installed." return 0 fi LOGI "Installing acme.sh..." cd ~ || return 1 # Ensure you can change to the home directory curl -s https://get.acme.sh | sh if [ $? -ne 0 ]; then LOGE "Installation of acme.sh failed." return 1 else LOGI "Installation of acme.sh succeeded." fi return 0 } ssl_cert_issue_main() { echo -e "${green}\t1.${plain} Get SSL (Domain)" echo -e "${green}\t2.${plain} Revoke" echo -e "${green}\t3.${plain} Force Renew" echo -e "${green}\t4.${plain} Show Existing Domains" echo -e "${green}\t5.${plain} Set Cert paths for the panel" echo -e "${green}\t6.${plain} Get SSL for IP Address (6-day cert, auto-renews)" echo -e "${green}\t0.${plain} Back to Main Menu" read -rp "Choose an option: " choice case "$choice" in 0) show_menu ;; 1) ssl_cert_issue ssl_cert_issue_main ;; 2) local domains=$(find /root/cert/ -mindepth 1 -maxdepth 1 -type d -exec basename {} \;) if [ -z "$domains" ]; then echo "No certificates found to revoke." else echo "Existing domains:" echo "$domains" read -rp "Please enter a domain from the list to revoke the certificate: " domain if echo "$domains" | grep -qw "$domain"; then ~/.acme.sh/acme.sh --revoke -d ${domain} LOGI "Certificate revoked for domain: $domain" else echo "Invalid domain entered." fi fi ssl_cert_issue_main ;; 3) local domains=$(find /root/cert/ -mindepth 1 -maxdepth 1 -type d -exec basename {} \;) if [ -z "$domains" ]; then echo "No certificates found to renew." else echo "Existing domains:" echo "$domains" read -rp "Please enter a domain from the list to renew the SSL certificate: " domain if echo "$domains" | grep -qw "$domain"; then ~/.acme.sh/acme.sh --renew -d ${domain} --force LOGI "Certificate forcefully renewed for domain: $domain" else echo "Invalid domain entered." fi fi ssl_cert_issue_main ;; 4) local domains=$(find /root/cert/ -mindepth 1 -maxdepth 1 -type d -exec basename {} \;) if [ -z "$domains" ]; then echo "No certificates found." else echo "Existing domains and their paths:" for domain in $domains; do local cert_path="/root/cert/${domain}/fullchain.pem" local key_path="/root/cert/${domain}/privkey.pem" if [[ -f "${cert_path}" && -f "${key_path}" ]]; then echo -e "Domain: ${domain}" echo -e "\tCertificate Path: ${cert_path}" echo -e "\tPrivate Key Path: ${key_path}" else echo -e "Domain: ${domain} - Certificate or Key missing." fi done fi ssl_cert_issue_main ;; 5) local domains=$(find /root/cert/ -mindepth 1 -maxdepth 1 -type d -exec basename {} \;) if [ -z "$domains" ]; then echo "No certificates found." else echo "Available domains:" echo "$domains" read -rp "Please choose a domain to set the panel paths: " domain if echo "$domains" | grep -qw "$domain"; then local webCertFile="/root/cert/${domain}/fullchain.pem" local webKeyFile="/root/cert/${domain}/privkey.pem" if [[ -f "${webCertFile}" && -f "${webKeyFile}" ]]; then ${xui_folder}/x-ui cert -webCert "$webCertFile" -webCertKey "$webKeyFile" echo "Panel paths set for domain: $domain" echo " - Certificate File: $webCertFile" echo " - Private Key File: $webKeyFile" restart else echo "Certificate or private key not found for domain: $domain." fi else echo "Invalid domain entered." fi fi ssl_cert_issue_main ;; 6) echo -e "${yellow}Let's Encrypt SSL Certificate for IP Address${plain}" echo -e "This will obtain a certificate for your server's IP using the shortlived profile." echo -e "${yellow}Certificate valid for ~6 days, auto-renews via acme.sh cron job.${plain}" echo -e "${yellow}Port 80 must be open and accessible from the internet.${plain}" confirm "Do you want to proceed?" "y" if [[ $? == 0 ]]; then ssl_cert_issue_for_ip fi ssl_cert_issue_main ;; *) echo -e "${red}Invalid option. Please select a valid number.${plain}\n" ssl_cert_issue_main ;; esac } ssl_cert_issue_for_ip() { LOGI "Starting automatic SSL certificate generation for server IP..." LOGI "Using Let's Encrypt shortlived profile (~6 days validity, auto-renews)" local existing_webBasePath=$(${xui_folder}/x-ui setting -show true | grep -Eo 'webBasePath: .+' | awk '{print $2}') local existing_port=$(${xui_folder}/x-ui setting -show true | grep -Eo 'port: .+' | awk '{print $2}') # Get server IP local server_ip=$(curl -s --max-time 3 https://api.ipify.org) if [ -z "$server_ip" ]; then server_ip=$(curl -s --max-time 3 https://4.ident.me) fi if [ -z "$server_ip" ]; then LOGE "Failed to get server IP address" return 1 fi LOGI "Server IP detected: ${server_ip}" # Ask for optional IPv6 local ipv6_addr="" read -rp "Do you have an IPv6 address to include? (leave empty to skip): " ipv6_addr ipv6_addr="${ipv6_addr// /}" # Trim whitespace # check for acme.sh first if ! command -v ~/.acme.sh/acme.sh &>/dev/null; then LOGI "acme.sh not found, installing..." install_acme if [ $? -ne 0 ]; then LOGE "Failed to install acme.sh" return 1 fi fi # install socat case "${release}" in ubuntu | debian | armbian) apt-get update >/dev/null 2>&1 && apt-get install socat -y >/dev/null 2>&1 ;; fedora | amzn | virtuozzo | rhel | almalinux | rocky | ol) dnf -y update >/dev/null 2>&1 && dnf -y install socat >/dev/null 2>&1 ;; centos) if [[ "${VERSION_ID}" =~ ^7 ]]; then yum -y update >/dev/null 2>&1 && yum -y install socat >/dev/null 2>&1 else dnf -y update >/dev/null 2>&1 && dnf -y install socat >/dev/null 2>&1 fi ;; arch | manjaro | parch) pacman -Sy --noconfirm socat >/dev/null 2>&1 ;; opensuse-tumbleweed | opensuse-leap) zypper refresh >/dev/null 2>&1 && zypper -q install -y socat >/dev/null 2>&1 ;; alpine) apk add socat curl openssl >/dev/null 2>&1 ;; *) LOGW "Unsupported OS for automatic socat installation" ;; esac # Create certificate directory certPath="/root/cert/ip" mkdir -p "$certPath" # Build domain arguments local domain_args="-d ${server_ip}" if [[ -n "$ipv6_addr" ]] && is_ipv6 "$ipv6_addr"; then domain_args="${domain_args} -d ${ipv6_addr}" LOGI "Including IPv6 address: ${ipv6_addr}" fi # Choose port for HTTP-01 listener (default 80, allow override) local WebPort="" read -rp "Port to use for ACME HTTP-01 listener (default 80): " WebPort WebPort="${WebPort:-80}" if ! [[ "${WebPort}" =~ ^[0-9]+$ ]] || ((WebPort < 1 || WebPort > 65535)); then LOGE "Invalid port provided. Falling back to 80." WebPort=80 fi LOGI "Using port ${WebPort} to issue certificate for IP: ${server_ip}" if [[ "${WebPort}" -ne 80 ]]; then LOGI "Reminder: Let's Encrypt still reaches port 80; forward external port 80 to ${WebPort} for validation." fi while true; do if is_port_in_use "${WebPort}"; then LOGI "Port ${WebPort} is currently in use." local alt_port="" read -rp "Enter another port for acme.sh standalone listener (leave empty to abort): " alt_port alt_port="${alt_port// /}" if [[ -z "${alt_port}" ]]; then LOGE "Port ${WebPort} is busy; cannot proceed with issuance." return 1 fi if ! [[ "${alt_port}" =~ ^[0-9]+$ ]] || ((alt_port < 1 || alt_port > 65535)); then LOGE "Invalid port provided." return 1 fi WebPort="${alt_port}" continue else LOGI "Port ${WebPort} is free and ready for standalone validation." break fi done # Reload command - restarts panel after renewal local reloadCmd="systemctl restart x-ui 2>/dev/null || rc-service x-ui restart 2>/dev/null" # issue the certificate for IP with shortlived profile ~/.acme.sh/acme.sh --set-default-ca --server letsencrypt --force ~/.acme.sh/acme.sh --issue \ ${domain_args} \ --standalone \ --server letsencrypt \ --certificate-profile shortlived \ --days 6 \ --httpport ${WebPort} \ --force if [ $? -ne 0 ]; then LOGE "Failed to issue certificate for IP: ${server_ip}" LOGE "Make sure port ${WebPort} is open and the server is accessible from the internet" # Cleanup acme.sh data for both IPv4 and IPv6 if specified rm -rf ~/.acme.sh/${server_ip} 2>/dev/null [[ -n "$ipv6_addr" ]] && rm -rf ~/.acme.sh/${ipv6_addr} 2>/dev/null rm -rf ${certPath} 2>/dev/null return 1 else LOGI "Certificate issued successfully for IP: ${server_ip}" fi # Install the certificate # Note: acme.sh may report "Reload error" and exit non-zero if reloadcmd fails, # but the cert files are still installed. We check for files instead of exit code. ~/.acme.sh/acme.sh --installcert -d ${server_ip} \ --key-file "${certPath}/privkey.pem" \ --fullchain-file "${certPath}/fullchain.pem" \ --reloadcmd "${reloadCmd}" 2>&1 || true # Verify certificate files exist (don't rely on exit code - reloadcmd failure causes non-zero) if [[ ! -f "${certPath}/fullchain.pem" || ! -f "${certPath}/privkey.pem" ]]; then LOGE "Certificate files not found after installation" # Cleanup acme.sh data for both IPv4 and IPv6 if specified rm -rf ~/.acme.sh/${server_ip} 2>/dev/null [[ -n "$ipv6_addr" ]] && rm -rf ~/.acme.sh/${ipv6_addr} 2>/dev/null rm -rf ${certPath} 2>/dev/null return 1 fi LOGI "Certificate files installed successfully" # enable auto-renew ~/.acme.sh/acme.sh --upgrade --auto-upgrade >/dev/null 2>&1 chmod 600 $certPath/privkey.pem 2>/dev/null chmod 644 $certPath/fullchain.pem 2>/dev/null # Set certificate paths for the panel local webCertFile="${certPath}/fullchain.pem" local webKeyFile="${certPath}/privkey.pem" if [[ -f "$webCertFile" && -f "$webKeyFile" ]]; then ${xui_folder}/x-ui cert -webCert "$webCertFile" -webCertKey "$webKeyFile" LOGI "Certificate configured for panel" LOGI " - Certificate File: $webCertFile" LOGI " - Private Key File: $webKeyFile" LOGI " - Validity: ~6 days (auto-renews via acme.sh cron)" echo -e "${green}Access URL: https://${server_ip}:${existing_port}${existing_webBasePath}${plain}" LOGI "Panel will restart to apply SSL certificate..." restart return 0 else LOGE "Certificate files not found after installation" return 1 fi } ssl_cert_issue() { local existing_webBasePath=$(${xui_folder}/x-ui setting -show true | grep -Eo 'webBasePath: .+' | awk '{print $2}') local existing_port=$(${xui_folder}/x-ui setting -show true | grep -Eo 'port: .+' | awk '{print $2}') # check for acme.sh first if ! command -v ~/.acme.sh/acme.sh &>/dev/null; then echo "acme.sh could not be found. we will install it" install_acme if [ $? -ne 0 ]; then LOGE "install acme failed, please check logs" exit 1 fi fi # install socat case "${release}" in ubuntu | debian | armbian) apt-get update >/dev/null 2>&1 && apt-get install socat -y >/dev/null 2>&1 ;; fedora | amzn | virtuozzo | rhel | almalinux | rocky | ol) dnf -y update >/dev/null 2>&1 && dnf -y install socat >/dev/null 2>&1 ;; centos) if [[ "${VERSION_ID}" =~ ^7 ]]; then yum -y update >/dev/null 2>&1 && yum -y install socat >/dev/null 2>&1 else dnf -y update >/dev/null 2>&1 && dnf -y install socat >/dev/null 2>&1 fi ;; arch | manjaro | parch) pacman -Sy --noconfirm socat >/dev/null 2>&1 ;; opensuse-tumbleweed | opensuse-leap) zypper refresh >/dev/null 2>&1 && zypper -q install -y socat >/dev/null 2>&1 ;; alpine) apk add socat curl openssl >/dev/null 2>&1 ;; *) LOGW "Unsupported OS for automatic socat installation" ;; esac if [ $? -ne 0 ]; then LOGE "install socat failed, please check logs" exit 1 else LOGI "install socat succeed..." fi # get the domain here, and we need to verify it local domain="" while true; do read -rp "Please enter your domain name: " domain domain="${domain// /}" # Trim whitespace if [[ -z "$domain" ]]; then LOGE "Domain name cannot be empty. Please try again." continue fi if ! is_domain "$domain"; then LOGE "Invalid domain format: ${domain}. Please enter a valid domain name." continue fi break done LOGD "Your domain is: ${domain}, checking it..." # check if there already exists a certificate local currentCert=$(~/.acme.sh/acme.sh --list | tail -1 | awk '{print $1}') if [ "${currentCert}" == "${domain}" ]; then local certInfo=$(~/.acme.sh/acme.sh --list) LOGE "System already has certificates for this domain. Cannot issue again. Current certificate details:" LOGI "$certInfo" exit 1 else LOGI "Your domain is ready for issuing certificates now..." fi # create a directory for the certificate certPath="/root/cert/${domain}" if [ ! -d "$certPath" ]; then mkdir -p "$certPath" else rm -rf "$certPath" mkdir -p "$certPath" fi # get the port number for the standalone server local WebPort=80 read -rp "Please choose which port to use (default is 80): " WebPort if [[ ${WebPort} -gt 65535 || ${WebPort} -lt 1 ]]; then LOGE "Your input ${WebPort} is invalid, will use default port 80." WebPort=80 fi LOGI "Will use port: ${WebPort} to issue certificates. Please make sure this port is open." # issue the certificate ~/.acme.sh/acme.sh --set-default-ca --server letsencrypt --force ~/.acme.sh/acme.sh --issue -d ${domain} --listen-v6 --standalone --httpport ${WebPort} --force if [ $? -ne 0 ]; then LOGE "Issuing certificate failed, please check logs." rm -rf ~/.acme.sh/${domain} exit 1 else LOGE "Issuing certificate succeeded, installing certificates..." fi reloadCmd="x-ui restart" LOGI "Default --reloadcmd for ACME is: ${yellow}x-ui restart" LOGI "This command will run on every certificate issue and renew." read -rp "Would you like to modify --reloadcmd for ACME? (y/n): " setReloadcmd if [[ "$setReloadcmd" == "y" || "$setReloadcmd" == "Y" ]]; then echo -e "\n${green}\t1.${plain} Preset: systemctl reload nginx ; x-ui restart" echo -e "${green}\t2.${plain} Input your own command" echo -e "${green}\t0.${plain} Keep default reloadcmd" read -rp "Choose an option: " choice case "$choice" in 1) LOGI "Reloadcmd is: systemctl reload nginx ; x-ui restart" reloadCmd="systemctl reload nginx ; x-ui restart" ;; 2) LOGD "It's recommended to put x-ui restart at the end, so it won't raise an error if other services fails" read -rp "Please enter your reloadcmd (example: systemctl reload nginx ; x-ui restart): " reloadCmd LOGI "Your reloadcmd is: ${reloadCmd}" ;; *) LOGI "Keep default reloadcmd" ;; esac fi # install the certificate ~/.acme.sh/acme.sh --installcert -d ${domain} \ --key-file /root/cert/${domain}/privkey.pem \ --fullchain-file /root/cert/${domain}/fullchain.pem --reloadcmd "${reloadCmd}" if [ $? -ne 0 ]; then LOGE "Installing certificate failed, exiting." rm -rf ~/.acme.sh/${domain} exit 1 else LOGI "Installing certificate succeeded, enabling auto renew..." fi # enable auto-renew ~/.acme.sh/acme.sh --upgrade --auto-upgrade if [ $? -ne 0 ]; then LOGE "Auto renew failed, certificate details:" ls -lah cert/* chmod 600 $certPath/privkey.pem chmod 644 $certPath/fullchain.pem exit 1 else LOGI "Auto renew succeeded, certificate details:" ls -lah cert/* chmod 600 $certPath/privkey.pem chmod 644 $certPath/fullchain.pem fi # Prompt user to set panel paths after successful certificate installation read -rp "Would you like to set this certificate for the panel? (y/n): " setPanel if [[ "$setPanel" == "y" || "$setPanel" == "Y" ]]; then local webCertFile="/root/cert/${domain}/fullchain.pem" local webKeyFile="/root/cert/${domain}/privkey.pem" if [[ -f "$webCertFile" && -f "$webKeyFile" ]]; then ${xui_folder}/x-ui cert -webCert "$webCertFile" -webCertKey "$webKeyFile" LOGI "Panel paths set for domain: $domain" LOGI " - Certificate File: $webCertFile" LOGI " - Private Key File: $webKeyFile" echo -e "${green}Access URL: https://${domain}:${existing_port}${existing_webBasePath}${plain}" restart else LOGE "Error: Certificate or private key file not found for domain: $domain." fi else LOGI "Skipping panel path setting." fi } ssl_cert_issue_CF() { local existing_webBasePath=$(${xui_folder}/x-ui setting -show true | grep -Eo 'webBasePath: .+' | awk '{print $2}') local existing_port=$(${xui_folder}/x-ui setting -show true | grep -Eo 'port: .+' | awk '{print $2}') LOGI "****** Instructions for Use ******" LOGI "Follow the steps below to complete the process:" LOGI "1. Cloudflare Registered E-mail." LOGI "2. Cloudflare Global API Key." LOGI "3. The Domain Name." LOGI "4. Once the certificate is issued, you will be prompted to set the certificate for the panel (optional)." LOGI "5. The script also supports automatic renewal of the SSL certificate after installation." confirm "Do you confirm the information and wish to proceed? [y/n]" "y" if [ $? -eq 0 ]; then # Check for acme.sh first if ! command -v ~/.acme.sh/acme.sh &>/dev/null; then echo "acme.sh could not be found. We will install it." install_acme if [ $? -ne 0 ]; then LOGE "Install acme failed, please check logs." exit 1 fi fi CF_Domain="" LOGD "Please set a domain name:" read -rp "Input your domain here: " CF_Domain LOGD "Your domain name is set to: ${CF_Domain}" # Set up Cloudflare API details CF_GlobalKey="" CF_AccountEmail="" LOGD "Please set the API key:" read -rp "Input your key here: " CF_GlobalKey LOGD "Your API key is: ${CF_GlobalKey}" LOGD "Please set up registered email:" read -rp "Input your email here: " CF_AccountEmail LOGD "Your registered email address is: ${CF_AccountEmail}" # Set the default CA to Let's Encrypt ~/.acme.sh/acme.sh --set-default-ca --server letsencrypt --force if [ $? -ne 0 ]; then LOGE "Default CA, Let'sEncrypt fail, script exiting..." exit 1 fi export CF_Key="${CF_GlobalKey}" export CF_Email="${CF_AccountEmail}" # Issue the certificate using Cloudflare DNS ~/.acme.sh/acme.sh --issue --dns dns_cf -d ${CF_Domain} -d *.${CF_Domain} --log --force if [ $? -ne 0 ]; then LOGE "Certificate issuance failed, script exiting..." exit 1 else LOGI "Certificate issued successfully, Installing..." fi # Install the certificate certPath="/root/cert/${CF_Domain}" if [ -d "$certPath" ]; then rm -rf ${certPath} fi mkdir -p ${certPath} if [ $? -ne 0 ]; then LOGE "Failed to create directory: ${certPath}" exit 1 fi reloadCmd="x-ui restart" LOGI "Default --reloadcmd for ACME is: ${yellow}x-ui restart" LOGI "This command will run on every certificate issue and renew." read -rp "Would you like to modify --reloadcmd for ACME? (y/n): " setReloadcmd if [[ "$setReloadcmd" == "y" || "$setReloadcmd" == "Y" ]]; then echo -e "\n${green}\t1.${plain} Preset: systemctl reload nginx ; x-ui restart" echo -e "${green}\t2.${plain} Input your own command" echo -e "${green}\t0.${plain} Keep default reloadcmd" read -rp "Choose an option: " choice case "$choice" in 1) LOGI "Reloadcmd is: systemctl reload nginx ; x-ui restart" reloadCmd="systemctl reload nginx ; x-ui restart" ;; 2) LOGD "It's recommended to put x-ui restart at the end, so it won't raise an error if other services fails" read -rp "Please enter your reloadcmd (example: systemctl reload nginx ; x-ui restart): " reloadCmd LOGI "Your reloadcmd is: ${reloadCmd}" ;; *) LOGI "Keep default reloadcmd" ;; esac fi ~/.acme.sh/acme.sh --installcert -d ${CF_Domain} -d *.${CF_Domain} \ --key-file ${certPath}/privkey.pem \ --fullchain-file ${certPath}/fullchain.pem --reloadcmd "${reloadCmd}" if [ $? -ne 0 ]; then LOGE "Certificate installation failed, script exiting..." exit 1 else LOGI "Certificate installed successfully, Turning on automatic updates..." fi # Enable auto-update ~/.acme.sh/acme.sh --upgrade --auto-upgrade if [ $? -ne 0 ]; then LOGE "Auto update setup failed, script exiting..." exit 1 else LOGI "The certificate is installed and auto-renewal is turned on. Specific information is as follows:" ls -lah ${certPath}/* chmod 600 ${certPath}/privkey.pem chmod 644 ${certPath}/fullchain.pem fi # Prompt user to set panel paths after successful certificate installation read -rp "Would you like to set this certificate for the panel? (y/n): " setPanel if [[ "$setPanel" == "y" || "$setPanel" == "Y" ]]; then local webCertFile="${certPath}/fullchain.pem" local webKeyFile="${certPath}/privkey.pem" if [[ -f "$webCertFile" && -f "$webKeyFile" ]]; then ${xui_folder}/x-ui cert -webCert "$webCertFile" -webCertKey "$webKeyFile" LOGI "Panel paths set for domain: $CF_Domain" LOGI " - Certificate File: $webCertFile" LOGI " - Private Key File: $webKeyFile" echo -e "${green}Access URL: https://${CF_Domain}:${existing_port}${existing_webBasePath}${plain}" restart else LOGE "Error: Certificate or private key file not found for domain: $CF_Domain." fi else LOGI "Skipping panel path setting." fi else show_menu fi } run_speedtest() { # Check if Speedtest is already installed if ! command -v speedtest &>/dev/null; then # If not installed, determine installation method if command -v snap &>/dev/null; then # Use snap to install Speedtest echo "Installing Speedtest using snap..." snap install speedtest else # Fallback to using package managers local pkg_manager="" local speedtest_install_script="" if command -v dnf &>/dev/null; then pkg_manager="dnf" speedtest_install_script="https://packagecloud.io/install/repositories/ookla/speedtest-cli/script.rpm.sh" elif command -v yum &>/dev/null; then pkg_manager="yum" speedtest_install_script="https://packagecloud.io/install/repositories/ookla/speedtest-cli/script.rpm.sh" elif command -v apt-get &>/dev/null; then pkg_manager="apt-get" speedtest_install_script="https://packagecloud.io/install/repositories/ookla/speedtest-cli/script.deb.sh" elif command -v apt &>/dev/null; then pkg_manager="apt" speedtest_install_script="https://packagecloud.io/install/repositories/ookla/speedtest-cli/script.deb.sh" fi if [[ -z $pkg_manager ]]; then echo "Error: Package manager not found. You may need to install Speedtest manually." return 1 else echo "Installing Speedtest using $pkg_manager..." curl -s $speedtest_install_script | bash $pkg_manager install -y speedtest fi fi fi speedtest } ip_validation() { ipv6_regex="^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))$" ipv4_regex="^((25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]?|0)\.){3}(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]?|0)$" } iplimit_main() { echo -e "\n${green}\t1.${plain} Install Fail2ban and configure IP Limit" echo -e "${green}\t2.${plain} Change Ban Duration" echo -e "${green}\t3.${plain} Unban Everyone" echo -e "${green}\t4.${plain} Ban Logs" echo -e "${green}\t5.${plain} Ban an IP Address" echo -e "${green}\t6.${plain} Unban an IP Address" echo -e "${green}\t7.${plain} Real-Time Logs" echo -e "${green}\t8.${plain} Service Status" echo -e "${green}\t9.${plain} Service Restart" echo -e "${green}\t10.${plain} Uninstall Fail2ban and IP Limit" echo -e "${green}\t0.${plain} Back to Main Menu" read -rp "Choose an option: " choice case "$choice" in 0) show_menu ;; 1) confirm "Proceed with installation of Fail2ban & IP Limit?" "y" if [[ $? == 0 ]]; then install_iplimit else iplimit_main fi ;; 2) read -rp "Please enter new Ban Duration in Minutes [default 30]: " NUM if [[ $NUM =~ ^[0-9]+$ ]]; then create_iplimit_jails ${NUM} if [[ $release == "alpine" ]]; then rc-service fail2ban restart else systemctl restart fail2ban fi else echo -e "${red}${NUM} is not a number! Please, try again.${plain}" fi iplimit_main ;; 3) confirm "Proceed with Unbanning everyone from IP Limit jail?" "y" if [[ $? == 0 ]]; then fail2ban-client reload --restart --unban 3x-ipl truncate -s 0 "${iplimit_banned_log_path}" echo -e "${green}All users Unbanned successfully.${plain}" iplimit_main else echo -e "${yellow}Cancelled.${plain}" fi iplimit_main ;; 4) show_banlog iplimit_main ;; 5) read -rp "Enter the IP address you want to ban: " ban_ip ip_validation if [[ $ban_ip =~ $ipv4_regex || $ban_ip =~ $ipv6_regex ]]; then fail2ban-client set 3x-ipl banip "$ban_ip" echo -e "${green}IP Address ${ban_ip} has been banned successfully.${plain}" else echo -e "${red}Invalid IP address format! Please try again.${plain}" fi iplimit_main ;; 6) read -rp "Enter the IP address you want to unban: " unban_ip ip_validation if [[ $unban_ip =~ $ipv4_regex || $unban_ip =~ $ipv6_regex ]]; then fail2ban-client set 3x-ipl unbanip "$unban_ip" echo -e "${green}IP Address ${unban_ip} has been unbanned successfully.${plain}" else echo -e "${red}Invalid IP address format! Please try again.${plain}" fi iplimit_main ;; 7) tail -f /var/log/fail2ban.log iplimit_main ;; 8) service fail2ban status iplimit_main ;; 9) if [[ $release == "alpine" ]]; then rc-service fail2ban restart else systemctl restart fail2ban fi iplimit_main ;; 10) remove_iplimit iplimit_main ;; *) echo -e "${red}Invalid option. Please select a valid number.${plain}\n" iplimit_main ;; esac } install_iplimit() { if ! command -v fail2ban-client &>/dev/null; then echo -e "${green}Fail2ban is not installed. Installing now...!${plain}\n" # Check the OS and install necessary packages case "${release}" in ubuntu) apt-get update if [[ "${os_version}" -ge 24 ]]; then apt-get install python3-pip -y python3 -m pip install pyasynchat --break-system-packages fi apt-get install fail2ban -y ;; debian) apt-get update if [ "$os_version" -ge 12 ]; then apt-get install -y python3-systemd fi apt-get install -y fail2ban ;; armbian) apt-get update && apt-get install fail2ban -y ;; fedora | amzn | virtuozzo | rhel | almalinux | rocky | ol) dnf -y update && dnf -y install fail2ban ;; centos) if [[ "${VERSION_ID}" =~ ^7 ]]; then yum update -y && yum install epel-release -y yum -y install fail2ban else dnf -y update && dnf -y install fail2ban fi ;; arch | manjaro | parch) pacman -Syu --noconfirm fail2ban ;; alpine) apk add fail2ban ;; *) echo -e "${red}Unsupported operating system. Please check the script and install the necessary packages manually.${plain}\n" exit 1 ;; esac if ! command -v fail2ban-client &>/dev/null; then echo -e "${red}Fail2ban installation failed.${plain}\n" exit 1 fi echo -e "${green}Fail2ban installed successfully!${plain}\n" else echo -e "${yellow}Fail2ban is already installed.${plain}\n" fi echo -e "${green}Configuring IP Limit...${plain}\n" # make sure there's no conflict for jail files iplimit_remove_conflicts # Check if log file exists if ! test -f "${iplimit_banned_log_path}"; then touch ${iplimit_banned_log_path} fi # Check if service log file exists so fail2ban won't return error if ! test -f "${iplimit_log_path}"; then touch ${iplimit_log_path} fi # Create the iplimit jail files # we didn't pass the bantime here to use the default value create_iplimit_jails # Launching fail2ban if [[ $release == "alpine" ]]; then if [[ $(rc-service fail2ban status | grep -F 'status: started' -c) == 0 ]]; then rc-service fail2ban start else rc-service fail2ban restart fi rc-update add fail2ban else if ! systemctl is-active --quiet fail2ban; then systemctl start fail2ban else systemctl restart fail2ban fi systemctl enable fail2ban fi echo -e "${green}IP Limit installed and configured successfully!${plain}\n" before_show_menu } remove_iplimit() { echo -e "${green}\t1.${plain} Only remove IP Limit configurations" echo -e "${green}\t2.${plain} Uninstall Fail2ban and IP Limit" echo -e "${green}\t0.${plain} Back to Main Menu" read -rp "Choose an option: " num case "$num" in 1) rm -f /etc/fail2ban/filter.d/3x-ipl.conf rm -f /etc/fail2ban/action.d/3x-ipl.conf rm -f /etc/fail2ban/jail.d/3x-ipl.conf if [[ $release == "alpine" ]]; then rc-service fail2ban restart else systemctl restart fail2ban fi echo -e "${green}IP Limit removed successfully!${plain}\n" before_show_menu ;; 2) rm -rf /etc/fail2ban if [[ $release == "alpine" ]]; then rc-service fail2ban stop else systemctl stop fail2ban fi case "${release}" in ubuntu | debian | armbian) apt-get remove -y fail2ban apt-get purge -y fail2ban -y apt-get autoremove -y ;; fedora | amzn | virtuozzo | rhel | almalinux | rocky | ol) dnf remove fail2ban -y dnf autoremove -y ;; centos) if [[ "${VERSION_ID}" =~ ^7 ]]; then yum remove fail2ban -y yum autoremove -y else dnf remove fail2ban -y dnf autoremove -y fi ;; arch | manjaro | parch) pacman -Rns --noconfirm fail2ban ;; alpine) apk del fail2ban ;; *) echo -e "${red}Unsupported operating system. Please uninstall Fail2ban manually.${plain}\n" exit 1 ;; esac echo -e "${green}Fail2ban and IP Limit removed successfully!${plain}\n" before_show_menu ;; 0) show_menu ;; *) echo -e "${red}Invalid option. Please select a valid number.${plain}\n" remove_iplimit ;; esac } show_banlog() { local system_log="/var/log/fail2ban.log" echo -e "${green}Checking ban logs...${plain}\n" if [[ $release == "alpine" ]]; then if [[ $(rc-service fail2ban status | grep -F 'status: started' -c) == 0 ]]; then echo -e "${red}Fail2ban service is not running!${plain}\n" return 1 fi else if ! systemctl is-active --quiet fail2ban; then echo -e "${red}Fail2ban service is not running!${plain}\n" return 1 fi fi if [[ -f "$system_log" ]]; then echo -e "${green}Recent system ban activities from fail2ban.log:${plain}" grep "3x-ipl" "$system_log" | grep -E "Ban|Unban" | tail -n 10 || echo -e "${yellow}No recent system ban activities found${plain}" echo "" fi if [[ -f "${iplimit_banned_log_path}" ]]; then echo -e "${green}3X-IPL ban log entries:${plain}" if [[ -s "${iplimit_banned_log_path}" ]]; then grep -v "INIT" "${iplimit_banned_log_path}" | tail -n 10 || echo -e "${yellow}No ban entries found${plain}" else echo -e "${yellow}Ban log file is empty${plain}" fi else echo -e "${red}Ban log file not found at: ${iplimit_banned_log_path}${plain}" fi echo -e "\n${green}Current jail status:${plain}" fail2ban-client status 3x-ipl || echo -e "${yellow}Unable to get jail status${plain}" } create_iplimit_jails() { # Use default bantime if not passed => 30 minutes local bantime="${1:-30}" # Uncomment 'allowipv6 = auto' in fail2ban.conf sed -i 's/#allowipv6 = auto/allowipv6 = auto/g' /etc/fail2ban/fail2ban.conf # On Debian 12+ fail2ban's default backend should be changed to systemd if [[ "${release}" == "debian" && ${os_version} -ge 12 ]]; then sed -i '0,/action =/s/backend = auto/backend = systemd/' /etc/fail2ban/jail.conf fi cat << EOF > /etc/fail2ban/jail.d/3x-ipl.conf [3x-ipl] enabled=true backend=auto filter=3x-ipl action=3x-ipl logpath=${iplimit_log_path} maxretry=2 findtime=32 bantime=${bantime}m EOF cat << EOF > /etc/fail2ban/filter.d/3x-ipl.conf [Definition] datepattern = ^%%Y/%%m/%%d %%H:%%M:%%S failregex = \[LIMIT_IP\]\s*Email\s*=\s*.+\s*\|\|\s*Disconnecting OLD IP\s*=\s*\s*\|\|\s*Timestamp\s*=\s*\d+ ignoreregex = EOF cat << EOF > /etc/fail2ban/action.d/3x-ipl.conf [INCLUDES] before = iptables-allports.conf [Definition] actionstart = -N f2b- -A f2b- -j -I -p -j f2b- actionstop = -D -p -j f2b- -X f2b- actioncheck = -n -L | grep -q 'f2b-[ \t]' actionban = -I f2b- 1 -s -j echo "\$(date +"%%Y/%%m/%%d %%H:%%M:%%S") BAN [Email] = [IP] = banned for seconds." >> ${iplimit_banned_log_path} actionunban = -D f2b- -s -j echo "\$(date +"%%Y/%%m/%%d %%H:%%M:%%S") UNBAN [Email] = [IP] = unbanned." >> ${iplimit_banned_log_path} [Init] name = default protocol = tcp chain = INPUT EOF echo -e "${green}Ip Limit jail files created with a bantime of ${bantime} minutes.${plain}" } iplimit_remove_conflicts() { local jail_files=( /etc/fail2ban/jail.conf /etc/fail2ban/jail.local ) for file in "${jail_files[@]}"; do # Check for [3x-ipl] config in jail file then remove it if test -f "${file}" && grep -qw '3x-ipl' ${file}; then sed -i "/\[3x-ipl\]/,/^$/d" ${file} echo -e "${yellow}Removing conflicts of [3x-ipl] in jail (${file})!${plain}\n" fi done } SSH_port_forwarding() { local URL_lists=( "https://api4.ipify.org" "https://ipv4.icanhazip.com" "https://v4.api.ipinfo.io/ip" "https://ipv4.myexternalip.com/raw" "https://4.ident.me" "https://check-host.net/ip" ) local server_ip="" for ip_address in "${URL_lists[@]}"; do local response=$(curl -s -w "\n%{http_code}" --max-time 3 "${ip_address}" 2>/dev/null) local http_code=$(echo "$response" | tail -n1) local ip_result=$(echo "$response" | head -n-1 | tr -d '[:space:]') if [[ "${http_code}" == "200" && -n "${ip_result}" ]]; then server_ip="${ip_result}" break fi done local existing_webBasePath=$(${xui_folder}/x-ui setting -show true | grep -Eo 'webBasePath: .+' | awk '{print $2}') local existing_port=$(${xui_folder}/x-ui setting -show true | grep -Eo 'port: .+' | awk '{print $2}') local existing_listenIP=$(${xui_folder}/x-ui setting -getListen true | grep -Eo 'listenIP: .+' | awk '{print $2}') local existing_cert=$(${xui_folder}/x-ui setting -getCert true | grep -Eo 'cert: .+' | awk '{print $2}') local existing_key=$(${xui_folder}/x-ui setting -getCert true | grep -Eo 'key: .+' | awk '{print $2}') local config_listenIP="" local listen_choice="" if [[ -n "$existing_cert" && -n "$existing_key" ]]; then echo -e "${green}Panel is secure with SSL.${plain}" before_show_menu fi if [[ -z "$existing_cert" && -z "$existing_key" && (-z "$existing_listenIP" || "$existing_listenIP" == "0.0.0.0") ]]; then echo -e "\n${red}Warning: No Cert and Key found! The panel is not secure.${plain}" echo "Please obtain a certificate or set up SSH port forwarding." fi if [[ -n "$existing_listenIP" && "$existing_listenIP" != "0.0.0.0" && (-z "$existing_cert" && -z "$existing_key") ]]; then echo -e "\n${green}Current SSH Port Forwarding Configuration:${plain}" echo -e "Standard SSH command:" echo -e "${yellow}ssh -L 2222:${existing_listenIP}:${existing_port} root@${server_ip}${plain}" echo -e "\nIf using SSH key:" echo -e "${yellow}ssh -i -L 2222:${existing_listenIP}:${existing_port} root@${server_ip}${plain}" echo -e "\nAfter connecting, access the panel at:" echo -e "${yellow}http://localhost:2222${existing_webBasePath}${plain}" fi echo -e "\nChoose an option:" echo -e "${green}1.${plain} Set listen IP" echo -e "${green}2.${plain} Clear listen IP" echo -e "${green}0.${plain} Back to Main Menu" read -rp "Choose an option: " num case "$num" in 1) if [[ -z "$existing_listenIP" || "$existing_listenIP" == "0.0.0.0" ]]; then echo -e "\nNo listenIP configured. Choose an option:" echo -e "1. Use default IP (127.0.0.1)" echo -e "2. Set a custom IP" read -rp "Select an option (1 or 2): " listen_choice config_listenIP="127.0.0.1" [[ "$listen_choice" == "2" ]] && read -rp "Enter custom IP to listen on: " config_listenIP ${xui_folder}/x-ui setting -listenIP "${config_listenIP}" >/dev/null 2>&1 echo -e "${green}listen IP has been set to ${config_listenIP}.${plain}" echo -e "\n${green}SSH Port Forwarding Configuration:${plain}" echo -e "Standard SSH command:" echo -e "${yellow}ssh -L 2222:${config_listenIP}:${existing_port} root@${server_ip}${plain}" echo -e "\nIf using SSH key:" echo -e "${yellow}ssh -i -L 2222:${config_listenIP}:${existing_port} root@${server_ip}${plain}" echo -e "\nAfter connecting, access the panel at:" echo -e "${yellow}http://localhost:2222${existing_webBasePath}${plain}" restart else config_listenIP="${existing_listenIP}" echo -e "${green}Current listen IP is already set to ${config_listenIP}.${plain}" fi ;; 2) ${xui_folder}/x-ui setting -listenIP 0.0.0.0 >/dev/null 2>&1 echo -e "${green}Listen IP has been cleared.${plain}" restart ;; 0) show_menu ;; *) echo -e "${red}Invalid option. Please select a valid number.${plain}\n" SSH_port_forwarding ;; esac } show_usage() { echo -e "┌────────────────────────────────────────────────────────────────┐ │ ${blue}x-ui control menu usages (subcommands):${plain} │ │ │ │ ${blue}x-ui${plain} - Admin Management Script │ │ ${blue}x-ui start${plain} - Start │ │ ${blue}x-ui stop${plain} - Stop │ │ ${blue}x-ui restart${plain} - Restart │ | ${blue}x-ui restart-xray${plain} - Restart Xray │ │ ${blue}x-ui status${plain} - Current Status │ │ ${blue}x-ui settings${plain} - Current Settings │ │ ${blue}x-ui enable${plain} - Enable Autostart on OS Startup │ │ ${blue}x-ui disable${plain} - Disable Autostart on OS Startup │ │ ${blue}x-ui log${plain} - Check logs │ │ ${blue}x-ui banlog${plain} - Check Fail2ban ban logs │ │ ${blue}x-ui update${plain} - Update │ │ ${blue}x-ui update-all-geofiles${plain} - Update all geo files │ │ ${blue}x-ui legacy${plain} - Legacy version │ │ ${blue}x-ui install${plain} - Install │ │ ${blue}x-ui uninstall${plain} - Uninstall │ └────────────────────────────────────────────────────────────────┘" } show_menu() { echo -e " ╔────────────────────────────────────────────────╗ │ ${green}3X-UI Panel Management Script${plain} │ │ ${green}0.${plain} Exit Script │ │────────────────────────────────────────────────│ │ ${green}1.${plain} Install │ │ ${green}2.${plain} Update │ │ ${green}3.${plain} Update Menu │ │ ${green}4.${plain} Legacy Version │ │ ${green}5.${plain} Uninstall │ │────────────────────────────────────────────────│ │ ${green}6.${plain} Reset Username & Password │ │ ${green}7.${plain} Reset Web Base Path │ │ ${green}8.${plain} Reset Settings │ │ ${green}9.${plain} Change Port │ │ ${green}10.${plain} View Current Settings │ │────────────────────────────────────────────────│ │ ${green}11.${plain} Start │ │ ${green}12.${plain} Stop │ │ ${green}13.${plain} Restart │ | ${green}14.${plain} Restart Xray │ │ ${green}15.${plain} Check Status │ │ ${green}16.${plain} Logs Management │ │────────────────────────────────────────────────│ │ ${green}17.${plain} Enable Autostart │ │ ${green}18.${plain} Disable Autostart │ │────────────────────────────────────────────────│ │ ${green}19.${plain} SSL Certificate Management │ │ ${green}20.${plain} Cloudflare SSL Certificate │ │ ${green}21.${plain} IP Limit Management │ │ ${green}22.${plain} Firewall Management │ │ ${green}23.${plain} SSH Port Forwarding Management │ │────────────────────────────────────────────────│ │ ${green}24.${plain} Enable BBR │ │ ${green}25.${plain} Update Geo Files │ │ ${green}26.${plain} Speedtest by Ookla │ ╚────────────────────────────────────────────────╝ " show_status echo && read -rp "Please enter your selection [0-26]: " num case "${num}" in 0) exit 0 ;; 1) check_uninstall && install ;; 2) check_install && update ;; 3) check_install && update_menu ;; 4) check_install && legacy_version ;; 5) check_install && uninstall ;; 6) check_install && reset_user ;; 7) check_install && reset_webbasepath ;; 8) check_install && reset_config ;; 9) check_install && set_port ;; 10) check_install && check_config ;; 11) check_install && start ;; 12) check_install && stop ;; 13) check_install && restart ;; 14) check_install && restart_xray ;; 15) check_install && status ;; 16) check_install && show_log ;; 17) check_install && enable ;; 18) check_install && disable ;; 19) ssl_cert_issue_main ;; 20) ssl_cert_issue_CF ;; 21) iplimit_main ;; 22) firewall_menu ;; 23) SSH_port_forwarding ;; 24) bbr_menu ;; 25) update_geo ;; 26) run_speedtest ;; *) LOGE "Please enter the correct number [0-26]" ;; esac } if [[ $# > 0 ]]; then case $1 in "start") check_install 0 && start 0 ;; "stop") check_install 0 && stop 0 ;; "restart") check_install 0 && restart 0 ;; "restart-xray") check_install 0 && restart_xray 0 ;; "status") check_install 0 && status 0 ;; "settings") check_install 0 && check_config 0 ;; "enable") check_install 0 && enable 0 ;; "disable") check_install 0 && disable 0 ;; "log") check_install 0 && show_log 0 ;; "banlog") check_install 0 && show_banlog 0 ;; "update") check_install 0 && update 0 ;; "legacy") check_install 0 && legacy_version 0 ;; "install") check_uninstall 0 && install 0 ;; "uninstall") check_install 0 && uninstall 0 ;; "update-all-geofiles") check_install 0 && update_all_geofiles 0 && restart 0 ;; *) show_usage ;; esac else show_menu fi ================================================ FILE: xray/api.go ================================================ // Package xray provides integration with the Xray proxy core. // It includes API client functionality, configuration management, traffic monitoring, // and process control for Xray instances. package xray import ( "context" "encoding/json" "fmt" "math" "regexp" "time" "github.com/mhsanaei/3x-ui/v2/logger" "github.com/mhsanaei/3x-ui/v2/util/common" "github.com/xtls/xray-core/app/proxyman/command" statsService "github.com/xtls/xray-core/app/stats/command" "github.com/xtls/xray-core/common/protocol" "github.com/xtls/xray-core/common/serial" "github.com/xtls/xray-core/infra/conf" "github.com/xtls/xray-core/proxy/shadowsocks" "github.com/xtls/xray-core/proxy/shadowsocks_2022" "github.com/xtls/xray-core/proxy/trojan" "github.com/xtls/xray-core/proxy/vless" "github.com/xtls/xray-core/proxy/vmess" "google.golang.org/grpc" "google.golang.org/grpc/credentials/insecure" ) // XrayAPI is a gRPC client for managing Xray core configuration, inbounds, outbounds, and statistics. type XrayAPI struct { HandlerServiceClient *command.HandlerServiceClient StatsServiceClient *statsService.StatsServiceClient grpcClient *grpc.ClientConn isConnected bool } // Init connects to the Xray API server and initializes handler and stats service clients. func (x *XrayAPI) Init(apiPort int) error { if apiPort <= 0 || apiPort > math.MaxUint16 { return fmt.Errorf("invalid Xray API port: %d", apiPort) } addr := fmt.Sprintf("127.0.0.1:%d", apiPort) conn, err := grpc.NewClient(addr, grpc.WithTransportCredentials(insecure.NewCredentials())) if err != nil { return fmt.Errorf("failed to connect to Xray API: %w", err) } x.grpcClient = conn x.isConnected = true hsClient := command.NewHandlerServiceClient(conn) ssClient := statsService.NewStatsServiceClient(conn) x.HandlerServiceClient = &hsClient x.StatsServiceClient = &ssClient return nil } // Close closes the gRPC connection and resets the XrayAPI client state. func (x *XrayAPI) Close() { if x.grpcClient != nil { x.grpcClient.Close() } x.HandlerServiceClient = nil x.StatsServiceClient = nil x.isConnected = false } // AddInbound adds a new inbound configuration to the Xray core via gRPC. func (x *XrayAPI) AddInbound(inbound []byte) error { client := *x.HandlerServiceClient conf := new(conf.InboundDetourConfig) err := json.Unmarshal(inbound, conf) if err != nil { logger.Debug("Failed to unmarshal inbound:", err) return err } config, err := conf.Build() if err != nil { logger.Debug("Failed to build inbound Detur:", err) return err } inboundConfig := command.AddInboundRequest{Inbound: config} _, err = client.AddInbound(context.Background(), &inboundConfig) return err } // DelInbound removes an inbound configuration from the Xray core by tag. func (x *XrayAPI) DelInbound(tag string) error { client := *x.HandlerServiceClient _, err := client.RemoveInbound(context.Background(), &command.RemoveInboundRequest{ Tag: tag, }) return err } // AddUser adds a user to an inbound in the Xray core using the specified protocol and user data. func (x *XrayAPI) AddUser(Protocol string, inboundTag string, user map[string]any) error { var account *serial.TypedMessage switch Protocol { case "vmess": account = serial.ToTypedMessage(&vmess.Account{ Id: user["id"].(string), }) case "vless": vlessAccount := &vless.Account{ Id: user["id"].(string), Flow: user["flow"].(string), } // Add testseed if provided if testseedVal, ok := user["testseed"]; ok { if testseedArr, ok := testseedVal.([]any); ok && len(testseedArr) >= 4 { testseed := make([]uint32, len(testseedArr)) for i, v := range testseedArr { if num, ok := v.(float64); ok { testseed[i] = uint32(num) } } vlessAccount.Testseed = testseed } else if testseedArr, ok := testseedVal.([]uint32); ok && len(testseedArr) >= 4 { vlessAccount.Testseed = testseedArr } } // Add testpre if provided (for outbound, but can be in user for compatibility) if testpreVal, ok := user["testpre"]; ok { if testpre, ok := testpreVal.(float64); ok && testpre > 0 { vlessAccount.Testpre = uint32(testpre) } else if testpre, ok := testpreVal.(uint32); ok && testpre > 0 { vlessAccount.Testpre = testpre } } account = serial.ToTypedMessage(vlessAccount) case "trojan": account = serial.ToTypedMessage(&trojan.Account{ Password: user["password"].(string), }) case "shadowsocks": var ssCipherType shadowsocks.CipherType switch user["cipher"].(string) { case "aes-128-gcm": ssCipherType = shadowsocks.CipherType_AES_128_GCM case "aes-256-gcm": ssCipherType = shadowsocks.CipherType_AES_256_GCM case "chacha20-poly1305", "chacha20-ietf-poly1305": ssCipherType = shadowsocks.CipherType_CHACHA20_POLY1305 case "xchacha20-poly1305", "xchacha20-ietf-poly1305": ssCipherType = shadowsocks.CipherType_XCHACHA20_POLY1305 default: ssCipherType = shadowsocks.CipherType_NONE } if ssCipherType != shadowsocks.CipherType_NONE { account = serial.ToTypedMessage(&shadowsocks.Account{ Password: user["password"].(string), CipherType: ssCipherType, }) } else { account = serial.ToTypedMessage(&shadowsocks_2022.ServerConfig{ Key: user["password"].(string), Email: user["email"].(string), }) } default: return nil } client := *x.HandlerServiceClient _, err := client.AlterInbound(context.Background(), &command.AlterInboundRequest{ Tag: inboundTag, Operation: serial.ToTypedMessage(&command.AddUserOperation{ User: &protocol.User{ Email: user["email"].(string), Account: account, }, }), }) return err } // RemoveUser removes a user from an inbound in the Xray core by email. func (x *XrayAPI) RemoveUser(inboundTag, email string) error { ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() op := &command.RemoveUserOperation{Email: email} req := &command.AlterInboundRequest{ Tag: inboundTag, Operation: serial.ToTypedMessage(op), } _, err := (*x.HandlerServiceClient).AlterInbound(ctx, req) if err != nil { return fmt.Errorf("failed to remove user: %w", err) } return nil } // GetTraffic queries traffic statistics from the Xray core, optionally resetting counters. func (x *XrayAPI) GetTraffic(reset bool) ([]*Traffic, []*ClientTraffic, error) { if x.grpcClient == nil { return nil, nil, common.NewError("xray api is not initialized") } trafficRegex := regexp.MustCompile(`(inbound|outbound)>>>([^>]+)>>>traffic>>>(downlink|uplink)`) clientTrafficRegex := regexp.MustCompile(`user>>>([^>]+)>>>traffic>>>(downlink|uplink)`) ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) defer cancel() if x.StatsServiceClient == nil { return nil, nil, common.NewError("xray StatusServiceClient is not initialized") } resp, err := (*x.StatsServiceClient).QueryStats(ctx, &statsService.QueryStatsRequest{Reset_: reset}) if err != nil { logger.Debug("Failed to query Xray stats:", err) return nil, nil, err } tagTrafficMap := make(map[string]*Traffic) emailTrafficMap := make(map[string]*ClientTraffic) for _, stat := range resp.GetStat() { if matches := trafficRegex.FindStringSubmatch(stat.Name); len(matches) == 4 { processTraffic(matches, stat.Value, tagTrafficMap) } else if matches := clientTrafficRegex.FindStringSubmatch(stat.Name); len(matches) == 3 { processClientTraffic(matches, stat.Value, emailTrafficMap) } } return mapToSlice(tagTrafficMap), mapToSlice(emailTrafficMap), nil } // processTraffic aggregates a traffic stat into trafficMap using regex matches and value. func processTraffic(matches []string, value int64, trafficMap map[string]*Traffic) { isInbound := matches[1] == "inbound" tag := matches[2] isDown := matches[3] == "downlink" if tag == "api" { return } traffic, ok := trafficMap[tag] if !ok { traffic = &Traffic{ IsInbound: isInbound, IsOutbound: !isInbound, Tag: tag, } trafficMap[tag] = traffic } if isDown { traffic.Down = value } else { traffic.Up = value } } // processClientTraffic updates clientTrafficMap with upload/download values for a client email. func processClientTraffic(matches []string, value int64, clientTrafficMap map[string]*ClientTraffic) { email := matches[1] isDown := matches[2] == "downlink" traffic, ok := clientTrafficMap[email] if !ok { traffic = &ClientTraffic{Email: email} clientTrafficMap[email] = traffic } if isDown { traffic.Down = value } else { traffic.Up = value } } // mapToSlice converts a map of pointers to a slice of pointers. func mapToSlice[T any](m map[string]*T) []*T { result := make([]*T, 0, len(m)) for _, v := range m { result = append(result, v) } return result } ================================================ FILE: xray/client_traffic.go ================================================ package xray // ClientTraffic represents traffic statistics and limits for a specific client. // It tracks upload/download usage, expiry times, and online status for inbound clients. type ClientTraffic struct { Id int `json:"id" form:"id" gorm:"primaryKey;autoIncrement"` InboundId int `json:"inboundId" form:"inboundId"` Enable bool `json:"enable" form:"enable"` Email string `json:"email" form:"email" gorm:"unique"` UUID string `json:"uuid" form:"uuid" gorm:"-"` SubId string `json:"subId" form:"subId" gorm:"-"` Up int64 `json:"up" form:"up"` Down int64 `json:"down" form:"down"` AllTime int64 `json:"allTime" form:"allTime"` ExpiryTime int64 `json:"expiryTime" form:"expiryTime"` Total int64 `json:"total" form:"total"` Reset int `json:"reset" form:"reset" gorm:"default:0"` LastOnline int64 `json:"lastOnline" form:"lastOnline" gorm:"default:0"` } ================================================ FILE: xray/config.go ================================================ package xray import ( "bytes" "github.com/mhsanaei/3x-ui/v2/util/json_util" ) // Config represents the complete Xray configuration structure. // It contains all sections of an Xray config file including inbounds, outbounds, routing, etc. type Config struct { LogConfig json_util.RawMessage `json:"log"` RouterConfig json_util.RawMessage `json:"routing"` DNSConfig json_util.RawMessage `json:"dns"` InboundConfigs []InboundConfig `json:"inbounds"` OutboundConfigs json_util.RawMessage `json:"outbounds"` Transport json_util.RawMessage `json:"transport"` Policy json_util.RawMessage `json:"policy"` API json_util.RawMessage `json:"api"` Stats json_util.RawMessage `json:"stats"` Reverse json_util.RawMessage `json:"reverse"` FakeDNS json_util.RawMessage `json:"fakedns"` Observatory json_util.RawMessage `json:"observatory"` BurstObservatory json_util.RawMessage `json:"burstObservatory"` Metrics json_util.RawMessage `json:"metrics"` } // Equals compares two Config instances for deep equality. func (c *Config) Equals(other *Config) bool { if len(c.InboundConfigs) != len(other.InboundConfigs) { return false } for i, inbound := range c.InboundConfigs { if !inbound.Equals(&other.InboundConfigs[i]) { return false } } if !bytes.Equal(c.LogConfig, other.LogConfig) { return false } if !bytes.Equal(c.RouterConfig, other.RouterConfig) { return false } if !bytes.Equal(c.DNSConfig, other.DNSConfig) { return false } if !bytes.Equal(c.OutboundConfigs, other.OutboundConfigs) { return false } if !bytes.Equal(c.Transport, other.Transport) { return false } if !bytes.Equal(c.Policy, other.Policy) { return false } if !bytes.Equal(c.API, other.API) { return false } if !bytes.Equal(c.Stats, other.Stats) { return false } if !bytes.Equal(c.Reverse, other.Reverse) { return false } if !bytes.Equal(c.FakeDNS, other.FakeDNS) { return false } if !bytes.Equal(c.Metrics, other.Metrics) { return false } return true } ================================================ FILE: xray/inbound.go ================================================ package xray import ( "bytes" "github.com/mhsanaei/3x-ui/v2/util/json_util" ) // InboundConfig represents an Xray inbound configuration. // It defines how Xray accepts incoming connections including protocol, port, and settings. type InboundConfig struct { Listen json_util.RawMessage `json:"listen"` // listen cannot be an empty string Port int `json:"port"` Protocol string `json:"protocol"` Settings json_util.RawMessage `json:"settings"` StreamSettings json_util.RawMessage `json:"streamSettings"` Tag string `json:"tag"` Sniffing json_util.RawMessage `json:"sniffing"` } // Equals compares two InboundConfig instances for deep equality. func (c *InboundConfig) Equals(other *InboundConfig) bool { if !bytes.Equal(c.Listen, other.Listen) { return false } if c.Port != other.Port { return false } if c.Protocol != other.Protocol { return false } if !bytes.Equal(c.Settings, other.Settings) { return false } if !bytes.Equal(c.StreamSettings, other.StreamSettings) { return false } if c.Tag != other.Tag { return false } if !bytes.Equal(c.Sniffing, other.Sniffing) { return false } return true } ================================================ FILE: xray/log_writer.go ================================================ package xray import ( "regexp" "runtime" "strings" "github.com/mhsanaei/3x-ui/v2/logger" ) // NewLogWriter returns a new LogWriter for processing Xray log output. func NewLogWriter() *LogWriter { return &LogWriter{} } // LogWriter processes and filters log output from the Xray process, handling crash detection and message filtering. type LogWriter struct { lastLine string } // Write processes and filters log output from the Xray process, handling crash detection and message filtering. func (lw *LogWriter) Write(m []byte) (n int, err error) { crashRegex := regexp.MustCompile(`(?i)(panic|exception|stack trace|fatal error)`) // Convert the data to a string message := strings.TrimSpace(string(m)) msgLowerAll := strings.ToLower(message) // Suppress noisy Windows process-kill signal that surfaces as exit status 1 if runtime.GOOS == "windows" && strings.Contains(msgLowerAll, "exit status 1") { return len(m), nil } // Check if the message contains a crash if crashRegex.MatchString(message) { logger.Debug("Core crash detected:\n", message) lw.lastLine = message err1 := writeCrashReport(m) if err1 != nil { logger.Error("Unable to write crash report:", err1) } return len(m), nil } regex := regexp.MustCompile(`^(\d{4}/\d{2}/\d{2} \d{2}:\d{2}:\d{2}\.\d{6}) \[([^\]]+)\] (.+)$`) messages := strings.SplitSeq(message, "\n") for msg := range messages { matches := regex.FindStringSubmatch(msg) if len(matches) > 3 { level := matches[2] msgBody := matches[3] msgBodyLower := strings.ToLower(msgBody) if strings.Contains(msgBodyLower, "tls handshake error") || strings.Contains(msgBodyLower, "connection ends") { logger.Debug("XRAY: " + msgBody) lw.lastLine = "" continue } if strings.Contains(msgBodyLower, "failed") { logger.Error("XRAY: " + msgBody) } else { switch level { case "Debug": logger.Debug("XRAY: " + msgBody) case "Info": logger.Info("XRAY: " + msgBody) case "Warning": logger.Warning("XRAY: " + msgBody) case "Error": logger.Error("XRAY: " + msgBody) default: logger.Debug("XRAY: " + msg) } } lw.lastLine = "" } else if msg != "" { msgLower := strings.ToLower(msg) if strings.Contains(msgLower, "tls handshake error") || strings.Contains(msgLower, "connection ends") { logger.Debug("XRAY: " + msg) lw.lastLine = msg continue } if strings.Contains(msgLower, "failed") { logger.Error("XRAY: " + msg) } else { logger.Debug("XRAY: " + msg) } lw.lastLine = msg } } return len(m), nil } ================================================ FILE: xray/process.go ================================================ package xray import ( "bytes" "encoding/json" "errors" "fmt" "io/fs" "os" "os/exec" "runtime" "strings" "syscall" "time" "github.com/mhsanaei/3x-ui/v2/config" "github.com/mhsanaei/3x-ui/v2/logger" "github.com/mhsanaei/3x-ui/v2/util/common" ) // GetBinaryName returns the Xray binary filename for the current OS and architecture. func GetBinaryName() string { return fmt.Sprintf("xray-%s-%s", runtime.GOOS, runtime.GOARCH) } // GetBinaryPath returns the full path to the Xray binary executable. func GetBinaryPath() string { return config.GetBinFolderPath() + "/" + GetBinaryName() } // GetConfigPath returns the path to the Xray configuration file in the binary folder. func GetConfigPath() string { return config.GetBinFolderPath() + "/config.json" } // GetGeositePath returns the path to the geosite data file used by Xray. func GetGeositePath() string { return config.GetBinFolderPath() + "/geosite.dat" } // GetGeoipPath returns the path to the geoip data file used by Xray. func GetGeoipPath() string { return config.GetBinFolderPath() + "/geoip.dat" } // GetIPLimitLogPath returns the path to the IP limit log file. func GetIPLimitLogPath() string { return config.GetLogFolder() + "/3xipl.log" } // GetIPLimitBannedLogPath returns the path to the banned IP log file. func GetIPLimitBannedLogPath() string { return config.GetLogFolder() + "/3xipl-banned.log" } // GetIPLimitBannedPrevLogPath returns the path to the previous banned IP log file. func GetIPLimitBannedPrevLogPath() string { return config.GetLogFolder() + "/3xipl-banned.prev.log" } // GetAccessPersistentLogPath returns the path to the persistent access log file. func GetAccessPersistentLogPath() string { return config.GetLogFolder() + "/3xipl-ap.log" } // GetAccessPersistentPrevLogPath returns the path to the previous persistent access log file. func GetAccessPersistentPrevLogPath() string { return config.GetLogFolder() + "/3xipl-ap.prev.log" } // GetAccessLogPath reads the Xray config and returns the access log file path. func GetAccessLogPath() (string, error) { config, err := os.ReadFile(GetConfigPath()) if err != nil { logger.Warningf("Failed to read configuration file: %s", err) return "", err } jsonConfig := map[string]any{} err = json.Unmarshal([]byte(config), &jsonConfig) if err != nil { logger.Warningf("Failed to parse JSON configuration: %s", err) return "", err } if jsonConfig["log"] != nil { jsonLog := jsonConfig["log"].(map[string]any) if jsonLog["access"] != nil { accessLogPath := jsonLog["access"].(string) return accessLogPath, nil } } return "", err } // stopProcess calls Stop on the given Process instance. func stopProcess(p *Process) { p.Stop() } // Process wraps an Xray process instance and provides management methods. type Process struct { *process } // NewProcess creates a new Xray process and sets up cleanup on garbage collection. func NewProcess(xrayConfig *Config) *Process { p := &Process{newProcess(xrayConfig)} runtime.SetFinalizer(p, stopProcess) return p } // NewTestProcess creates a new Xray process that uses a specific config file path. // Used for test runs (e.g. outbound test) so the main config.json is not overwritten. // The config file at configPath is removed when the process is stopped. func NewTestProcess(xrayConfig *Config, configPath string) *Process { p := &Process{newTestProcess(xrayConfig, configPath)} runtime.SetFinalizer(p, stopProcess) return p } type process struct { cmd *exec.Cmd version string apiPort int onlineClients []string config *Config configPath string // if set, use this path instead of GetConfigPath() and remove on Stop logWriter *LogWriter exitErr error startTime time.Time } // newProcess creates a new internal process struct for Xray. func newProcess(config *Config) *process { return &process{ version: "Unknown", config: config, logWriter: NewLogWriter(), startTime: time.Now(), } } // newTestProcess creates a process that writes and runs with a specific config path. func newTestProcess(config *Config, configPath string) *process { p := newProcess(config) p.configPath = configPath return p } // IsRunning returns true if the Xray process is currently running. func (p *process) IsRunning() bool { if p.cmd == nil || p.cmd.Process == nil { return false } if p.cmd.ProcessState == nil { return true } return false } // GetErr returns the last error encountered by the Xray process. func (p *process) GetErr() error { return p.exitErr } // GetResult returns the last log line or error from the Xray process. func (p *process) GetResult() string { if len(p.logWriter.lastLine) == 0 && p.exitErr != nil { return p.exitErr.Error() } return p.logWriter.lastLine } // GetVersion returns the version string of the Xray process. func (p *process) GetVersion() string { return p.version } // GetAPIPort returns the API port used by the Xray process. func (p *Process) GetAPIPort() int { return p.apiPort } // GetConfig returns the configuration used by the Xray process. func (p *Process) GetConfig() *Config { return p.config } // GetOnlineClients returns the list of online clients for the Xray process. func (p *Process) GetOnlineClients() []string { return p.onlineClients } // SetOnlineClients sets the list of online clients for the Xray process. func (p *Process) SetOnlineClients(users []string) { p.onlineClients = users } // GetUptime returns the uptime of the Xray process in seconds. func (p *Process) GetUptime() uint64 { return uint64(time.Since(p.startTime).Seconds()) } // refreshAPIPort updates the API port from the inbound configs. func (p *process) refreshAPIPort() { for _, inbound := range p.config.InboundConfigs { if inbound.Tag == "api" { p.apiPort = inbound.Port break } } } // refreshVersion updates the version string by running the Xray binary with -version. func (p *process) refreshVersion() { cmd := exec.Command(GetBinaryPath(), "-version") data, err := cmd.Output() if err != nil { p.version = "Unknown" } else { datas := bytes.Split(data, []byte(" ")) if len(datas) <= 1 { p.version = "Unknown" } else { p.version = string(datas[1]) } } } // Start launches the Xray process with the current configuration. func (p *process) Start() (err error) { if p.IsRunning() { return errors.New("xray is already running") } defer func() { if err != nil { logger.Error("Failure in running xray-core process: ", err) p.exitErr = err } }() data, err := json.MarshalIndent(p.config, "", " ") if err != nil { return common.NewErrorf("Failed to generate XRAY configuration files: %v", err) } err = os.MkdirAll(config.GetLogFolder(), 0o770) if err != nil { logger.Warningf("Failed to create log folder: %s", err) } configPath := GetConfigPath() if p.configPath != "" { configPath = p.configPath } err = os.WriteFile(configPath, data, fs.ModePerm) if err != nil { return common.NewErrorf("Failed to write configuration file: %v", err) } cmd := exec.Command(GetBinaryPath(), "-c", configPath) p.cmd = cmd cmd.Stdout = p.logWriter cmd.Stderr = p.logWriter go func() { err := cmd.Run() if err != nil { // On Windows, killing the process results in "exit status 1" which isn't an error for us if runtime.GOOS == "windows" { errStr := strings.ToLower(err.Error()) if strings.Contains(errStr, "exit status 1") { // Suppress noisy log on graceful stop p.exitErr = err return } } logger.Error("Failure in running xray-core:", err) p.exitErr = err } }() p.refreshVersion() p.refreshAPIPort() return nil } // Stop terminates the running Xray process. func (p *process) Stop() error { if !p.IsRunning() { return errors.New("xray is not running") } // Remove temporary config file used for test runs so main config is never touched if p.configPath != "" { if p.configPath != GetConfigPath() { // Check if file exists before removing if _, err := os.Stat(p.configPath); err == nil { _ = os.Remove(p.configPath) } } } if runtime.GOOS == "windows" { return p.cmd.Process.Kill() } else { return p.cmd.Process.Signal(syscall.SIGTERM) } } // writeCrashReport writes a crash report to the binary folder with a timestamped filename. func writeCrashReport(m []byte) error { crashReportPath := config.GetBinFolderPath() + "/core_crash_" + time.Now().Format("20060102_150405") + ".log" return os.WriteFile(crashReportPath, m, os.ModePerm) } ================================================ FILE: xray/traffic.go ================================================ package xray // Traffic represents network traffic statistics for Xray connections. // It tracks upload and download bytes for inbound or outbound traffic. type Traffic struct { IsInbound bool IsOutbound bool Tag string Up int64 Down int64 }